2018-02-16

RubyのCGIをGoogle Cloud Platformの無料枠で動かしたい ~(2)SELinuxの設定

SELinuxでCGIスクリプトが500エラーになる

RubyのCGIをGCP(Google Cloud Platform)の無料枠で動かしたい~(1)セットアップ の続きです。

CGIスクリプトをブラウザから実行しようとして 500 Internal Server Error が出る場合、SELinuxの設定変更が必要かもしれません。

対策として「SELinuxをオフにすれば動きます」という大胆なブログ記事も見かけますが、できることならセキュアな状態で使いたいものです。GCPは世界中から狙われやすそうですし。
SELinuxを有効にしたままCGIを動かせるよう設定します。

問題を切り分ける(本当にSELinuxのせい?)

SELinuxを一時的に停止して、本当にSELinuxのせいなのかを切り分けます。
# setenforce 0
# getenforce ←確認
Permissive
permissiveモードは完全無効化とは違い、アクセス制御は行ないませんがログを出力します。

まずは超簡単なシェルスクリプトを /var/www/cgi-bin/test.cgi あたりに配置してみます。
#!/bin/sh
echo Content-type:text/plain
echo
echo ok!
パーミッションは755にでもして、コマンドラインで実行できることを確認します。
# cd /var/www/cgi-bin
# ./test.cgi
Content-type:text/plain

ok!
シェル上では問題なく実行できるのに http://~/cgi-bin/test.cgi にブラウザでアクセスしてエラーが出るようなら、そもそもSELinuxとは別のところに問題がありそうです。
  • Apacheの設定は正しいですか? apachectl configtest の結果は?
  • httpd.conf編集後に systemctl reload httpd しました?
  • cgiファイルのパーミッションは適切ですか?(Apacheユーザに実行権限はあります?)

シェルスクリプトで問題なかったら、次はRubyのCGIスクリプトを /cgi-bin に置きます。
#!/usr/local/bin/ruby
puts "Content-type: text/plain"
puts
puts "Ruby!"
まずはコマンドラインで実行します。ここで動かないならRubyが正しく導入できていないことになります。
次にブラウザからアクセスしてみます。エラーになるなら、スペルミスやファイルパーミッションなどを確認します。

ここまで確認して問題がなかったなら、SELinuxを元に戻してみます。
# setenforce 1
# getenforce
Enforcing
これで再びエラーが出るようになったなら、さすがにSELinuxが原因と言えそうです。

SELinuxでCGIを動かすための基本設定 (httpd_enable_cgi)

そもそもCGIが無効になっていると動きません。
# getsebool httpd_enable_cgi
httpd_enable_cgi --> off
offになっていたらonにします。
# setsebool -P httpd_enable_cgi 1

SELinuxコンテキストの設定 (httpd_sys_script_exec_t)

SELinuxでは「コンテキスト」という概念があって、適切なコンテキストが設定されていないと動作が阻止されます。
CGIの場合、ファイルが httpd_sys_script_exec_t というタイプになっている必要があります。

コンテキストは ls コマンドの -Z オプションで表示できます。
# ls -aZ /var/www/cgi-bin
drwxr-xr-x. root root system_u:object_r:httpd_sys_script_exec_t:s0 .
drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 ..
-rwxr-xr-x. root root unconfined_u:object_r:user_home_t:s0 test.cgi
末尾が _t となっている部分がタイプです。test.cgiファイルは user_home_t タイプになっており、これではSELinuxに止められます。

chcon コマンドでタイプを変更します。
# chcon -t httpd_sys_script_exec_t test.cgi
# ls -Z test.cgi
-rwxr-xr-x. root root unconfined_u:object_r:httpd_sys_script_exec_t:s0 test.cgi

GCEのCentOS7なら、ここまですれば全く動かないということはないと思いますが、それでも動かない場合はログを見ながら原因を探ることになります。
# audit2allow -wa
ログの読み方と対処法はここでは説明しきれないので、ハマってしまったらがんばってください……。

/var/www/html 以下でもCGIスクリプトを動かしたい (httpd_sys_script_exec_t)

CGIファイルを /var/www/cgi-bin ではない場所に置きたい場合、先ほどと同じくCGIファイルのタイプを httpd_sys_script_exec_t にすればOKです。
# cd /var/www/html
# ls -Z test.cgi
-rwxr-xr-x. root root unconfined_u:object_r:httpd_sys_content_t:s0 test.cgi
# chcon -t httpd_sys_script_exec_t test.cgi
# ls -Z test.cgi
-rwxr-xr-x. root root unconfined_u:object_r:httpd_sys_script_exec_t:s0 test.cgi

/var/www/cgi-bin では動作するのに /var/www/html では動かない、という場合はApacheの設定を見直します。Options に ExecCGI は入ってますか?

SELinuxコンテキスト設定の永続化と適用 (restorecon)

ひととおり動作することが確認できたら、デフォルトのタイプ設定として永続化することができます。
パスは正規表現で指定します。
# semanage fcontext -a -t httpd_sys_script_exec_t '/var/www/html/.*\.cgi'
# semanage fcontext -lC
SELinux fcontext                                   type               Context
/var/www/html/.*\.cgi                              all files          system_u:object_r:httpd_sys_script_exec_t:s0
これで /var/www/html/ 以下の(サブディレクトリも含む)拡張子.cgiのファイルのデフォルトタイプが httpd_sys_script_exec_t になりました。

デフォルト設定すればいちいち chcon しなくても済むのですが、残念ながらファイルを新しく作ると自動的にデフォルトタイプになるわけではありません。
初期タイプは、ファイルの属するディレクトリから受け継ぎます。
# touch /var/www/html/test2.cgi
# ls -aZ /var/www/html
drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 .
drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 ..
-rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 test2.cgi
ファイルのデフォルトタイプを適用するには restorecon を使います。
# restorecon -v test2.cgi
restorecon reset /var/www/html/test2.cgi context unconfined_u:object_r:httpd_sys_content_t:s0->unconfined_u:object_r:httpd_sys_script_exec_t:s0
# ls -Z test2.cgi
-rwxr-xr-x. root root unconfined_u:object_r:httpd_sys_script_exec_t:s0 test2.cgi

結局ファイルを作るたびにタイプ変更のコマンドを叩く必要があるなら、デフォルト設定って何の役に立つの? という気持ちになるかもしれませんが、restorecon は chcon と違ってタイプを具体的に指定しなくて済むのはメリットです。

CGIスクリプトからファイルに書き込みたい (httpd_sys_rw_content_t)

CGIの実行は可能になったものの、CGIスクリプトからファイルに書き込もうとするとSELinuxによって阻止されます。
たとえ書き込み先のファイル権限が 666 であってもです。

書き込み先のファイルにはSELinuxのタイプ httpd_sys_rw_content_t が必要です。
ファイルをCGIスクリプトから作成したい場合は、書き込み先のディレクトリにも同タイプを設定します。

chcon で毎回設定してもいいですが、たとえば data という名前のディレクトリは一律でデータ用とするなら、デフォルト設定にしてもよいでしょう。
# semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/html(/.*)?/data(/.*)?'
これで /var/www/html 以下の data ディレクトリと、dataディレクトリ内のすべてのファイルやディレクトリのデフォルトタイプが httpd_sys_rw_content_t になります。

data ディレクトリを httpd_sys_rw_content_t タイプにしておくと、CGIスクリプトでデータファイルを新規に作成した場合、ファイルのタイプは自動的に httpd_sys_rw_content_t になります。新しく作ったファイルはディレクトリのタイプを受け継ぐためです。

CGIスクリプトから通信したい (httpd_can_network_connect)

CGIスクリプトからローカルファイルの読み書きは可能になりましたが、スクリプト内で通信をしようとするとSELinuxに阻止されます。
curlを呼んだりしたい場合は、通信の許可設定が必要です。
# setsebool -P httpd_can_network_connect 1

httpd関連だけSELinuxを止めたい(permissive_httpd_t)

とりあえず以上の設定でCGIスクリプトはひととおり動くはずですが、どうしてもエラーが解消しきれなくて時間がない場合、SELinuxを丸ごと permissive モードにするのではなく、httpd 関連のみ permissive にする方法もあります。
# semanage permissive -a httpd_t
# semodule -l | grep permissive
permissive_httpd_t      (null)
permissivedomains       (null)

問題が解決したら元に戻します。
# semanage permissive -d httpd_t

2018-02-02

RubyのCGIをGoogle Cloud Platformの無料枠で動かしたい ~(1)セットアップ

RubyのCGIをGCPで(無料で)動かしたい

自分で使う用のこまごまとしたWebアプリを動かすために手頃なレンタルサーバーを借りていたのですが、年契約の更新時期が来ました。
ふと思い出したのが昨春Google Cloud Platform(GCP)の無料枠拡大の話。2年目以降も無料で使えるのはかなり限られた構成になりますが、自分しか使わないサーバーなのでスペックは足りそう。乗り換えてみることにしました。

GCP(の無料枠)でPHPやRuby on Railsを動かすための情報はいくつか見つかったのですが、CGIの話は見当たらなかったので、作業記録やひっかかりポイントを書き残しておきます。

無料トライアルに申し込む

無料枠内しか使わないつもりでも、クレジットカードの登録は必要です。

申込みから初期セットアップまでの手順はいろんな方がまとめてくれているので、ここでは改めて書きません。たとえば下記などを参考に。

OSはCentOS 7にしました。個人的にはDebianのほうが慣れていますが仕事だとCentOSが多いので、これを機に馴染んでおこうかと。

他、ポイントとしては:
  • ゾーンはus-westを選びます。usしか無料にならないので、せめて日本から一番近い西海岸を
  • ディスクサイズはせっかくなので無料枠の上限である30GBに上げます
  • 固定(静的な外部)IPアドレスを割り当てます(1つなら無料)

最低限の初期設定

GCE(Google Compute Engine)のVMインスタンスが起ち上がったら、Web上の管理画面からブラウザ上で動くSSHクライアントを起動できます。
最終的には手元のPC等のネイティブSSHクライアントから入ったほうが快適なのですが、とりあえずの作業はブラウザからでも事足ります。ちょっとレスポンスが遅いのが難点ですが。

ブラウザSSHだとbash上でCtrl+Wを叩くと、1語消えるかわりにブラウザが「このサイトを離れてもよろしいですか?」と訊いてくる問題もありますが、ChromeならSSH for Google Cloud Platform 拡張機能をインストールすればCtrl+WもCtrl+NもCtrl+Tも叩き放題です。

最低限、次のあたりを設定しておきます。
  • 22番ポートをファイアウォールで遮断。セキュリティ対策
  • 代わりに適当なポートでSSHで入れるように
  • タイムゾーンを日本に
    $ sudo timedatectl set-timezone Asia/Tokyo

ほかはお好みで。

Apacheをインストール

$ sudo yum install httpd
/etc/httpd/conf/httpd.conf などを適宜編集したら、
$ sudo systemctl enable httpd

DNSやSSLの設定は後回しにして、いったん手元のPC等から http://IPアドレス/ でwelcomeページを表示できることを確認します。
表示できない場合は、Apacheやファイアウォールなどの設定を見直します。

ところで CentOS 7 ではサービスの制御は systemctl を使いますが、httpd.conf を編集した後に反映させるのは graceful ではなく reload です。
$ sudo systemctl graceful httpd
Unknown operation 'graceful'.
$ sudo systemctl reload httpd
$ (反映された)

/usr/lib/systemd/system/httpd.service には次のように定義されており、ちゃんと graceful してくれます。
(略)
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful
(略)



yumのRubyは古い

Rubyもyumで…と思いきや、バージョンがずいぶん古いのでした。
$ yum info ruby
(略)
Available Packages
Name        : ruby
Arch        : x86_64
Version     : 2.0.0.648
Release     : 30.el7
(略)
仕方ないのでrbenvを入れて自分でビルドします。
ちょっと重い作業になりますが、レンタルサーバーと違って新しいバージョンが使えるというのはうれしいものです。いまだに1.8しか使えないレンタルサーバーとかありますし……。

Rubyビルドの事前準備(パッケージ導入・スワップファイル作成)

ビルドに必要なパッケージを入れます。
$ sudo yum -y install git gcc openssl-devel readline-devel
(他にも何か入れたかも。ビルド中にエラーが出たら適宜yum installしてください)

最初、このままビルドに進んだら途中でメモリ不足になってしまいました。
無料枠のf1-microインスタンスのメモリは0.6GB。国民機PC-9801の1000倍ですが、それでも足りないようです。

スワップファイルを作ります。
$ sudo su -
# dd if=/dev/zero of=/swapfile bs=1M count=1024
# chmod 600 /swapfile
# mkswap /swapfile
# swapon /swapfile
# free -m
           total       used       free     shared buff/cache  available
Mem:         588        148        319          4        119        323
Swap:       1023          0       1023

1GBにしたのにはキリがいいからですが、推奨値はRAMの2倍なので1.2GBのがよかったのかもしれません。ただ、1GBでもRuby 2.5.0のビルドには問題ありませんでした。

今後もスワップファイルを常用したければ /etc/fstab に書きます。
/swapfile       swap    swap    defaults        0 0

rbenvをシステム全体で使えるようにインストール

rbenvの基本的な導入方法は https://github.com/rbenv/rbenv に書かれているとおりですが、~/.rbenv/ にインストールしてしまうとCGIで使うにはちょっと不便です。システム全体で使えるよう /opt/rbenv に導入することにします。

rbenvを /usr/local 以下に導入した話をよく見かけますが、FHS(Filesystem Hierarchy Standard)では /usr/local には任意のディレクトリを作ってはならないと規定されています。パッケージ名でディレクトリを作りたければ /opt を使うそうです。

.bash_profile に書く設定は代わりに /etc/profile.d/ 以下に置けば、自動的に全員にロードされます。
# git clone https://github.com/rbenv/rbenv.git /opt/rbenv
# echo 'export RBENV_ROOT="/opt/rbenv"' >> /etc/profile.d/rbenv.sh
# echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"' >> /etc/profile.d/rbenv.sh
# echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh
# . /etc/profile.d/rbenv.sh

Rubyをビルド・symlink作成

Rubyのバージョンは事情に応じて選びます。
特に事情もないなら最新版(現時点では2.5.0)でよいかと。
# mkdir -p "$(rbenv root)"/plugins
# git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
# rbenv install 2.5.0
(20分くらいかかりました)
# rbenv global 2.5.0
# ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

このままでもRubyは実行できますが、CGIスクリプトのファイル1行目を次のように書くことになります。
#!/opt/rbenv/shims/ruby
ちょっと見慣れなすぎて覚えられなさそうなので、見知ったパスにシンボリックリンクをはってしまいます。
# ln -s /opt/rbenv/shims/ruby /usr/local/bin/ruby

SELinuxの設定変更

この状態でももうCGIスクリプトは動作するのですが、cgi-bin ディレクトリ以外に置くと 500 Internal Server Error になってしまいます。
/var/www/html 以下でもCGIスクリプトは動かしたい場合、SELinuxの設定変更が必要です。

長くなってきたので次回の記事で。