[debian] Debian GNU/Linux に Google Authenticator 認証でログインする

RSA SecurIDソフトウェアトークンのように、ワンタイムパスワード生成機の所有者のみにSSHログインを許可するための設定。できるだけセキュアにしたいVPCや社内ネットワークのゲートウェイサーバに設定することを想定した設定のメモです。

Google Authenticatorで2段階認証を行う [Debian]
Use Google Authenticator to login to a Linux PCが非常に参考になりました。特に後者の記事は丁寧ですばらしいです。

google-authenticator のインストール

Google code からコードをダウンロードする

sudo apt-get install mercurial subversion
cd ~/; hg clone https://code.google.com/p/google-authenticator/
cd google-authenticator/libpam

google-authenticator のビルド。libpam0g-dev がちゃんとインストールされていれば一発でmakeが通ると思います。無事ビルドできたらインストール。

sudo apt-get install make gcc libpam0g-dev libqrencode3
cd ~/google-authenticator/libpam
make
sudo make install

PAMの設定

/usr/share/pam-configs/ 以下にPAMの設定を作成する。こちらの設定のまま、>/usr/share/pam-configs/google-all と /usr/share/pam-configs/google-enough を作成。

PAMについては、http://d.hatena.ne.jp/int128/20090726/1248622071 が参考になりました。 今回の場合、既存の認証にgoogle-authenticatorを追加したいため、required である、google-all を利用することにします。 以下のコマンドを実行し、google-all を選択。

sudo pam-auth-update

SSHの設定

以下の設定を変更。ChallengeResponseAuthentication 以外の認証をnoにして、google-authenticator 以外でのログインを許可しないようにする。
ChallengeResponseAuthentication と UsePAM は、google-authenticator を利用するために必要。

PubkeyAuthentication no
PasswordAuthentication no
RhostsRSAAuthentication no
RSAAuthentication no
ChallengeResponseAuthentication yes
UsePAM yes

sshd の reload を忘れずに。この際、万が一設定をミスってログインできなくならないように、sudo su - になったターミナルをふたつほど開いておくと良いと思います。

sudo /etc/init.d/ssh reload

google-authenticator でキーを作成

$ google-authenticator
<QRCODEへのURL>

[         ]
[  QRコード ]
[         ]
Your new secret key is: 4M2HNEG3DC42XXXX
Your verification code is 5458***
Your emergency scratch codes are:
  949987**
  942234**
  296841**
  456148**
  794053**

Do you want me to update your "/home/<your name>/.google_authenticator" file (y/n) y

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases
your chances to notice or even prevent man-in-the-middle attacks (y/n) n

By default, tokens are good for 30 seconds and in order to compensate for
possible time-skew between the client and the server, we allow an extra
token before and after the current time. If you experience problems with poor
time synchronization, you can increase the window from its default
size of 1:30min to about 4min. Do you want to do so (y/n) n

If the computer that you are logging into isn't hardened against brute-force
login attempts, you can enable rate-limiting for the authentication module.
By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting (y/n) y

上記でサーバー側の設定は完了です。

Google Authenticator のインストール

  1. iPhoneApp Store で Google Authenticator をダウンロード
  2. アプリを起動し、右下の「+」で、設定を追加
  3. 画面下の「バーコードをスキャン」でサーバ側設定の最後で表示したバーコードを読み込む。

これで設定は完了。

SSHクライアントでログイン

ログインしてみると Verification を聞かれるようになる。

$ ssh localhost
Verification code: [Google Authenticator に表示されているワンタイムパスワードを入力]
Password: [UNIXパスワードを入力]

宿題

PubkeyAuthentication + Google Authenticator の認証にしたいのだけど、今回の設定では、PasswordAuthentication + Google Authenticator になってしまっている。 これでは、セキュリティが強化されたかというと、どっちもどっちな気がするので、今後の課題。

[rails] config/routes.rbでピリオドを含むパラメータをURLに使う

ホスト名やIPアドレス、数値など、URLのパラメータでピリオドを使おうとすると (.:format) がマッチしてしまってエラーとなる場合がある。
そういう場合は、下記のように :constraints オプションでパターンを正規表現で指定すれば良い。

#config/routes.rb
match '/show/:host/:path' => 'show#host', :constraints => {:host=>/[a-zA-Z0-9_-\.]+/}

詳細は、actionpack-3.1.3/lib/action_dispatch/routing/mapper.rb のコメントを参照。
http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html も。

datetime_select のオプション

scaffold でモデルを作成した際に、recorded_at:datetime といった datetime の型を指定したときなどに自動生成される、datetime_select ヘルパーについてのメモ。

何もオプションをしていしない場合は、

<%= form_for(@record) do |f| %>
  <%= f.datetime_select :recorded_at %>
<% end %>

以下のHTMLが出力されますが、

:
以下のように、オプションを指定すると、

 22   <div class="field">
 23     <%= f.label :recorded_at %><br />
 24     <%= f.datetime_select :recorded_at %>デフォルトのまま。<br>
 25     <%= f.datetime_select :recorded_at, :discard_hour => true, :discard_minute => true %>時と分を無効に。<br>
 26     <%= f.datetime_select :recorded_at, :use_month_numbers => true, :start_year => 2010, :end_year => Time.now.year, :date_separator => "/" %>月を数字にして、日付の区切りを/に。開始年と終了年を指定。<br>
 27     <%= f.datetime_select :recorded_at, :use_month_numbers => true, :start_year => 2010, :end_year => Time.now.year, :default => Time.at((Time.now.to_i/86400)*86400).utc %>時間を今日の00:00に変更。<br>
 28     <%= f.datetime_select :recorded_at, :start_year => 2010, :end_year => Time.now.year, :use_month_names => %w(睦月 如月 弥生 卯月 皐月 水無月 文月 葉月 長月 神無月 霜月 師走) %>月の表示を和暦に。
 29   </div>

次のようなHTMLが出力されます。




: デフォルトのまま。

時と分を無効に。

// : 月を数字にして、日付の区切りを/に。開始年と終了年を指定。

: 時間を今日の00:00に変更。

: 月の表示を和暦に。

和暦なんてまず使わないだろうけど、メモ。

詳細は、http://api.rubyonrails.org/ で datetime_select, select_month などで検索したり、
./actionpack-3.1.3/lib/action_view/helpers/date_helper.rb の datetime_select から読むとわかりやすいです。


追記:
一日の始まりは、Time.beginning_of_day でできるらしい。

session_store :active_record_store 時のデバッグTIPS

概要

rail3 で session_store :active_record_store とした場合、DBにsessionsテーブルが作成されるが、デバッグ目的にSELECTしてみても、BASE64エンコードされていて内容を簡単に確認できない。開発環境の場合、BASE64エンコードを止めて、デバッグしやすくするためのメモ。

結論としては、BASE64のencode/decodeしているコードを書き換えて、平文でDBに保存されるようにしました。以下そこに至るまでのメモ。

メモ

まず、MySQL5.6 には、TO_BASE64, FROM_BASE64があるので、SELECT FROM_BASE64(data) from sessions で行けそうだが、すべての環境がMySQL5.6では無いと思うし、環境(DB)に依存しない方法が無いか探ってみる。

理解のために、gems/activerecord-3.1.3/lib/active_record/session_store.rb を読むとBASE64エンコードされていることがわかる。

rails3 の session に対する操作は、rack-1.3.5/lib/rack/session/abstract/id.rb で定義されていて、active_record_store の場合は activerecord-3.1.3/lib/active_record/session_store.rb で実装されている。

#/var/lib/gems/1.8/gems/rack-1.3.5/lib/rack/session/abstract/id.rb 
# Rack::Session::Abstract::ID

137       # ID sets up a basic framework for implementing an id based sessioning
138       # service. Cookies sent to the client for maintaining sessions will only
139       # contain an id reference. Only #get_session and #set_session are
140       # required to be overwritten.
141       #
142       # All parameters are optional.
143       # * :key determines the name of the cookie, by default it is
144       #   'rack.session'
145       # * :path, :domain, :expire_after, :secure, and :httponly set the related
146       #   cookie options as by Rack::Response#add_cookie
147       # * :defer will not set a cookie in the response.
148       # * :renew (implementation dependent) will prompt the generation of a new
149       #   session id, and migration of data to be referenced at the new id. If
150       #   :defer is set, it will be overridden and the cookie will be set.
151       # * :sidbits sets the number of bits in length that a generated session
152       #   id will be.
#/var/lib/gems/1.8/gems/activerecord-3.1.3/lib/active_record/session_store.rb
 13   # The +session_id+ column should always be indexed for speedy lookups.
 14   # Session data is marshaled to the +data+ column in Base64 format.
 15   # If the data you write is larger than the column's size limit,
 16   # ActionController::SessionOverflowError will be raised.
<snip>
 34   # You may provide your own session class implementation, whether a
 35   # feature-packed Active Record or a bare-metal high-performance SQL
 36   # store, by setting
 37   #
 38   #   ActiveRecord::SessionStore.session_class = MySessionClass
 39
 40   # You must implement these methods:
 41   #
 42   #   self.find_by_session_id(session_id)
 43   #   initialize(hash_of_session_id_and_data, options_hash = {})
 44   #   attr_reader :session_id
 45   #   attr_accessor :data
 46   #   save
 47   #   destroy
<snip>
 51   class SessionStore < ActionDispatch::Session::AbstractStore
 52     module ClassMethods # :nodoc:
 53       def marshal(data)
 54         ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
 55       end
 56
 57       def unmarshal(data)
 58         Marshal.load(ActiveSupport::Base64.decode64(data)) if data
 59       end
<snip>
#ActiveRecord::SessionStore::SqlBypass
260       def save
261         return false unless loaded?
262         marshaled_data = self.class.marshal(data)
263         connect        = connection
264
265         if @new_record
266           @new_record = false
267           connect.update <<-end_sql, 'Create session'
268             INSERT INTO #{table_name} (
269               #{connect.quote_column_name(session_id_column)},
270               #{connect.quote_column_name(data_column)} )
271             VALUES (
272               #{connect.quote(session_id)},
273               #{connect.quote(marshaled_data)} )
274           end_sql
275         else
276           connect.update <<-end_sql, 'Update session'
277             UPDATE #{table_name}
278             SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)}
279             WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)}
280           end_sql
281         end
282       end

よって、任意のSession操作を行うためのクラスを書いて ActiveRecord::SessionStore.session_class で指定すれば良さそう。

ここで、負荷対策などのために凝ったことするのであれば自分で書いた方が良さそうだが、今回はデバッグできれば良いので、config/environments/development.rb の中で、marshal と unmarshal を書き換えてみる。

 #config/environments/development.rb
  1 <APPNAME>::Application.configure do
  2   # Settings specified here will take precedence over those in config/application.rb
  3
<snip>
 31   config.after_initialize do
 32     require 'active_record/session_store'
 33     module ActiveRecord
 34       class SessionStore
 35         module ClassMethods
 36           def marshal(data)
 37             Marshal.dump(data) if data
 38           end
 39           def unmarshal(data)
 40             Marshal.load(data) if data
 41           end
 42         end
 43       end
 44     end
 45   end
 46 end

これで試してみたら、無事読むことができました。

session[:test] = {:foo => :bar}
mysql> select * from sessions\G
*************************** 1. row ***************************
        id: 1
session_id: 6c4dd632c07d36934013849810a95879
      data:{:_csrf_token"1Gk+iPeVoIRWilIRulpoB3UKsh/QNdcZim91hd4SccAU=: test{foobar
created_at: 2012-01-19 14:35:50
updated_at: 2012-01-19 14:35:50
1 row in set (0.00 sec)

データベースに格納できないデータを扱った場合の備えとして、BASE64をしていると思われるため、mershalを書き換えるのは、development のみにしています。 session テーブルを BINARY型にしておくと良いのかな?

rails3の実行環境ごとの設定方法については、 http://stackoverflow.com/questions/4800028/rails-3-setting-custom-environment-variables が参考になりました。