2015年12月9日

freeipa+SSSDで2FAを使ってログインする話

この記事はLinux Advent Calendar 2015の9日目の記事です。

イントロ

freeipaでは通常のパスワード認証のほかにOne Time Password(OTP)を利用した2要素認証が簡単に利用できる。googleとかgithubとかでセキュリティ固くしたいときに使うアレだ。
これを利用するときには、従来だと「パスワード文字列とOTP文字列の順にくっつけた文字列」を渡していた。
たとえば以下のような状態だとすると
  • username: kmoriwak
  • password: password
  • OTPのトークンの値: 123123
以下のように入力する。
Login: kmoriwak
Password: password123123
これがRHEL7.2に含まれているpam_sss (sssd-client-1.13.0-40.el7.x86_64) とコンソールのloginやgdmでは以下のようになる。
Login: kmoriwak
First factor: password
Second factor: 123123
何故このように変更されたのか? を見ていこう。

freeipa

この10年ほど、認証周りの標準は Active Directory によって先導されてきた。ADに相当するものとしてはsambaがAD互換の実装を行っているが、UNIX/Linux世界に独特な要件のサポートはいまいちと言わざるをえない状況だった。
UNIX/Linux世界にはLDAP, Kerberos, DNS, PKIなどの基盤技術はあるので、freeipaはこれらを統合したAD相当の認証基盤を作成する。
https://www.freeipa.org/page/About から引用すると:

FreeIPA is an integrated security information management solution combining Linux (Fedora), 389 Directory Server, MIT Kerberos, NTP, DNS, Dogtag (Certificate System). It consists of a web interface and command-line administration tools.
freeipaでは単純なパスワード認証だけではなく、Kerberosを使ったシングルサインオン、OTPを利用した2要素認証をサポートしている。またUNIX/Linux世界向けの機能としては、bindの設定, sshdの公開鍵, sudoポリシーを管理する。

SSSD

SSSDはActive Directoryやfreeipa、LDAPなどによる「ドメイン」と接続し、PAMとNSSのインタフェースに対してドメイン修飾つきの認証を提供する。
たとえばADで AD.EXAMPLE.COM を運用し、freeipaで IPA.EXAMPLE.COM を運用している。さらにLDAP上にも別の名前空間でuid,gidをもっているようなときに、3つ全てのドメインの情報を利用して認証をおこなうことができる。
ドメイン修飾というのは kmoriwak@AD.EXAMPLE.COMと  kmoriwak@IPA.EXAMPLE.COM のようにドメインで重なる(が別のユーザにひもづく)名前がある場合に@以下にドメイン名を記載することでどのドメインかわかるようにするもので、まあだいたいメールのドメイン名みたいなものだ。
PAMとNSSが独立していると複数ドメインを扱うときにうまく連携できないためSSSDが導入され、さらにfreeipaとADとの連携を行う各種の機能や、次節で紹介するキャッシュ機能などが追加されている。

SSSDによるパスワードのキャッシュ

SSSDは認証のキャッシュを行う機能を持つ。いくつか目的があるが、可用性の向上と負荷の軽減が主なものである。
ユースケース1: 普段freeipaで認証するが、何かの問題でfreeipaに接続できない場合もログインサービスを継続したい
ユースケース2: ノートPCを持ち歩く場合に、普段はドメインのDirectory情報をつかった認証をおこなって、オフライン時にはキャッシュを利用して認証したい
ユースケース3: 頻繁に行われる認証をキャッシュしてDirectoryの負荷を下げたい

SSSDはPAM経由で平文のパスワードを受けつけ、これをSSHA2で加工して保存する。
キャッシュは各ドメインに対応したldbに保存されている。以下のようにtdbtoolで見える。
# tdbtool /var/lib/sss/db/cache_example.com.ldb show "DN=NAME=kmoriwak,CN=USERS,CN=EXAMPLE.COM,CN=SYSDB\00"
(中略)
[620] 39 30 33 5A 00 63 61 63  68 65 64 50 61 73 73 77  903Z.cac hedPassw
[630] 6F 72 64 00 01 00 00 00  6A 00 00 00 24 36 24 6B  ord.... j...$6$k
[640] 76 75 50 49 53 2E 41 4F  59 68 56 56 67 4E 4F 24  vuPIS.AO YhVVgNO$
[650] 33 4A 61 2F 36 63 34 4C  6B 77 7A 32 4B 64 6A 51  3Ja/6c4L kwz2KdjQ
[660] 34 73 63 5A 7A 6D 46 75  30 49 56 7A 4B 71 61 6F  4scZzmFu 0IVzKqao
[670] 4A 37 72 52 2F 54 76 53  6E 50 6E 55 6B 75 54 30  J7rR/TvS nPnUkuT0
[680] 6B 37 32 54 45 67 67 2E  41 5A 50 41 76 66 36 69  k72TEgg. AZPAvf6i
[690] 37 49 4E 54 34 64 38 39  46 4F 59 32 45 51 35 30  7INT4d89 FOY2EQ50
[6A0] 73 7A 30 33 66 30 00 6C  61 73 74 43 61 63 68 65  sz03f0.l astCache

(以下略)
オフライン時にどの程度の時間(24時間や48時間など)キャッシュを維持するかはsssdの設定次第で変更できるが、デフォルトでは制限がおこなわれていないためオフライン時の認証には向いている。

パスワードとOTPトークンを区別したいケース

OTPのトークンは設定によるが30秒や60秒でどんどん変わってしまうのでキャッシュをすることには意味がない。
さらに2要素認証を利用するにあたって、常に2要素を接続した文字列を利用しつづけることは現実的ではない。具体的には固定的なパスワードと連携したキーリングのアンロックやディレクトリの暗号化には共通鍵を使いたいのでOTPをそのまま適用することができない。
またオフライン時に認証を継続したい場合には、OTPのトークンを検証できないためこれを使わないで認証したい。入力された[パス ワードとOTPを組み合わせた文字列]をそのままハッシュ化してキャッシュすると次回オフライン時の認証は必ず失敗することになってしまう。
このためパス ワードとOTPトークンをなんとか区別したい。トークンの長さが常に一定であれば簡単なのだが、残念ながらそうではない。

freeipaへの認証とOTP

freeipaへの認証は基本的にKerberosへのログインである。OTPを利用する場合の動きについては http://www.freeipa.org/page/V4/OTP#Design に詳しいが、OTP over RADIUS を使って認証時にLDAP上のユーザ属性にもとづいてfreeipa組み込みのOTPに対応する他、サードパーティのOTPにも対応できるようになっている。
このため事前にトークンの文字列長を一般に確定させる方法は存在しない(トークン長が常に一定だとも仮定できない)。実装によってはKerberosのチャレンジとしてトークン長が帰ってくる (http://tools.ietf.org/html/rfc6560)ものもあるので、これを利用してSSSDとpam_sssがトークンを除去できることもある。freeipaに内蔵のHOTP,TOTP実装はトークン長を返すのでうまく動作できる。
力技での対応策としては入力文字列を最後から1文字ずつ削除してハッシュを作る作戦があるがあまり望ましくない。たとえば入力が16文字だとして、16,15,14...と文字数を減らしていきそれぞれのハッシュ値を計算してキャッシュに維持する。しかしこれを単純に適用すると短い文字列に対するハッシュを生成し、結果としてオフライン時の認証が脆弱になる危険がある。たとえば16文字の入力から12文字削って先頭4文字に対するハッシュを維持するようなことがあればオフライン認証に対しての総当たり攻撃が非常に簡単になるだろう。

解決策

ここで最初のふるまいにもどる。SSSDとpam_sssが協調し、事前にユーザ情報から認証のタイプを確認して2つの要素を分けて入力させることでパスワードとトークンの分離をするためのヒューリスティックは不要となる。
なおpam_conv()複数入力をサポートしておりgdmやloginは複数入力に対応するが、歴史的理由により1つの文字列しか認証用に入力できない処理系も多数存在する。そのためpam_sssの実装では、パスワードとトークンが接続された文字列と空文字列、パスワードとトークンを別々に入力の2種類に対応している。
sshdから使われる場合のみワークアラウンドとしてパスワードとトークンが接続された文字列が2回入力されるパターンに対応している。