[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 のインストール
- iPhone の App Store で Google Authenticator をダウンロード
- アプリを起動し、右下の「+」で、設定を追加
- 画面下の「バーコードをスキャン」でサーバ側設定の最後で表示したバーコードを読み込む。
これで設定は完了。
SSHクライアントでログイン
ログインしてみると Verification を聞かれるようになる。
$ ssh localhost Verification code: [Google Authenticator に表示されているワンタイムパスワードを入力] Password: [UNIXパスワードを入力]
[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 が参考になりました。