Chienomi

(ReasonSet Edge) ウェブアプリケーションの認証にSSHの鍵認証を活用する

技術::reasonset

今回から私らしい、私独自の、「そんなんアリかよ」と言いたくなるような奇抜な、しかし有効なテクニックの話は“ReasonSet Edge”というくくりでやることにした。

認証は難しい。

ウェブアプリケーションにおいて認証管理は常に大きなセキュリティリスクであり、そのリスクは至るところに潜んでいる。だから難しい。

そもそも、ウェブの認証機能は脆弱すぎる。IDとパスワードなんて何の役にも立たない。 もっとマシな認証機構を備えるべきだ。

そう、SSHのように。

SSHの認証

SSHの認証機構は色々あるのだが、ポピュラーかつ強力なのが「公開鍵認証」である。

名前は「公開鍵認証」だ。「秘密公開鍵認証」でも「非対称暗号鍵認証」でもない。「公開鍵認証」だ。これはとても重要なことだ。なぜならば、公開鍵認証において使われるのは公開鍵だけだからだ。

公開鍵認証がどのように行われるかは以前の記事で書いた

その認証はあくまでもサーバーがクライアントを検証するものであり、クライアントがサーバーを検証するものではない。 それは、SSHの経路暗号の中に含まれている機能であり、ユーザーが自ら検証しなければならない。

さて、SSH公開鍵認証は基本的に「当該ファイルを持っている」ことが認証の要件となるのだが、通信経路の安全性が高く、そのファイルを推測して認証を破るのが極めて困難である。 さらにいえば、OpenSSHにおいては当該ファイル自体を暗号化することができ、より安全である。そのパスワードはネットワーク上に流れないから、残るのはヒューマンエラーによって本人が流出させるケースだけになる。ショルダーハッキングにも強い。

さらにSSH鍵認証は認証に使用した公開鍵に対して到底のコマンドの実行を強制するCommandというオプションがある。これは、sshd_configによって設定可能なForceCommandを公開鍵に対するオプションとして設定できるものだ。

sshd_configの条件付きの適用と組み合わせることで、コマンドの実行以上のことは一切できないようにすることもできる。

さらに、Linux, *BSD, Mac OS X, Windows 10のいずれにも備わっているのも魅力だ。

SSHの認証を組み合わせる

とはいえ、SSHでの認証が直接ウェブで使えるわけではない。 ウェブ上の認証メソッドにSSHがあるわけではないからだ。

厳密な方法としてPermitOpenを使うという方法がある。 これは、OpenSSHの-Lオプションによって、ローカルマシンのポートからSSH経由でリモートマシンのポートに転送するときに、転送できるホストとポートを指定するものである。

ここに、「認証済みでしか使えないページを、ループバックインターフェイスに対してlistenするサーバー」として立てておくことが、SSHによって認証しなければそもそもページにアクセスすることすらできない、という状態にする。

sshd_configでは次のようにする。

PermitOpen localhost:8080

AuthorizedKeysのオプションとしても利用できる。

permitopen="localhost:8080"

もちろん、SSHを他に利用できないように制限しておくこと。

これだと「誰であるか」が特定できない。 簡単な方法は、ForceCommandによって次のようなスクリプトを実行することだ。

require 'openssl'

dig = nil

loop do
  dig = OpenSSL::Digest::SHA512.hexdigest(File.read("/dev/urandom", 4092))
  break unless File.exist? "/run/http_sessions/#{dig}"
end

puts dig

loop do
  now = Time.now.to_i + 300
  File.open("/run/http_sessions/#{dig}", "r+") do |f|
    f.flock(File::LOCK_EX)
    f.truncate(0)
    f.puts [now, ARGV[0]]
  end
  sleep 180
end

これによって「セッションIDの名前のファイルで、内容はセッションの有効期限+ユーザーID」というファイルが作られる。接続が切られると期限の更新がされなくなるのでセッションは消滅する。

セッションIDを持たないクライアントに対しては、PIN入力フォームを表示し、セッションの有効性とユーザーIDはファイルを読むことで確認する。

PIN+SSH

厳密にアクセスを制限する方法はユーザー的にも負担が大きく、ウェブアプリケーション制作者にはピンとこないかもしれない。そんなピンとこないアナタにはPINを使う方法を提案したい。

SSHでログインすると、次のように短い有効期限を持つPINコードを発行する。

pin = #... PINの生成方法は色々あるので各々

File.open("/run/http_sessions/#{pin}", "w") do |f|
  Marshal.dump({expires: (Time.now + (60 * 30)), id: ARGV[0]}, f)
end

puts pin

このPINは30分だけ有効である。 あとはログイン画面でPINを入力させる。それで認証に成功したら、あとは普通にアプリケーションとして管理すれば良い。

PINを使うことの良い点は

  • 有効期限が短いので漏洩に対するリスクが少ない
  • 同時に存在する有効なPINが少ないので攻撃が成功しにくい
  • パスワード使い回しによる延焼リスクがない
  • 辞書攻撃が効かない
  • 強度が一定に保たれる (脆弱なパスワードを気にしなくて良い)
  • ユーザーデータベースを漏洩させたときの影響範囲が下がる
  • スマホでも比較的入力しやすい

といったメリットがある。少なくともこの方法は普通にユーザーIDとパスワードを使うよりも安定して安全である。

注意点

SSHでの接続は常に目的以外の利用ができないようにオプションを設定しなければならない。

また、公開鍵の追加登録ができないとユーザーは秘密鍵を複数端末間で共有してしまうので、簡単に追加登録できるようにしなければならない。

PINを発行するだけの方法だと、専用ユーザーを作成し、例えば次のように設定するのが良い。

Match User webauth
  DisableForwarding yes
  MaxSessions 1
  AcceptEnv no
  PermitListen no
  PermitOpen no
  PermitTTY no
  PermitEmptyPasswords no
  PermitTunnel no
  PermitUserRC no
  AuthenticationMethods publickey

アカウントごとに異なる引数をコマンドに渡すため、ForceCommandsshd_configにかくのではなく、鍵オプションにするのが良いだろう。

他の認証手段

やはりWeb Authenticationによる生体認証が強い。 生体認証はセキュリティ強度の向上というよりも利便性の向上のほうがメインではあるのだが。

ただ、Web Authenticationによる暗証は実際のところは公開鍵認証である。 というのは、あれは「指紋によって秘密鍵を暗号化する」というものなので、公開鍵認証においてパスフレーズの代わりに指紋を使っているだけで、サーバーとの間では公開鍵認証になっているのだ。

しかし「指紋認証が可能である」という条件をつけると、どうやっても認証できない環境が相当に出てくる(特にLinuxにとっては厳しい)ので、現状まだ採用しにくい。

SSH+PINという方法は一見回りくどく見えるが、実際はそれほど難しくなく、コード量もだいぶ減らせる(もちろん、フレームワークに依存するようなことはない前提で、である)上にセキュリティ強度を上げられるので、なかなか悪くない。

ユーザーとしては慣れない手間をかける感じになってしまうので、強力なセキュリティを欲しがるケースで適用すると良いだろう。