Kerberos認証を使ってみる (OpenSSH)

[前回記事](https://qiita.com/kanatatsu64/items/ec50707002c5dbd7330d)の続きです。

# モチベーション

前回はサーバーのセットアップまで完了しました。ここでは、その環境を使ってSSO(Single Sign On)を実現していきます。

完成予定図です。

<img src=https://qiita-image-store.s3.amazonaws.com/0/126131/57bb60f1-9c11-8b39-679c-742715f9e6d8.png width=70% />

大量に作ったVMをSSOを使って簡単に管理しようという魂胆です。

環境は、KDCとVM達はGoogle Cloud Platform上のDebian GNU/Linux 9 (stretch)を使っています。また、ClientはMacOS High Sierraです。

# Kerberosについての簡単な説明

前回の記事では環境構築をひたすらやったので、それぞれのサーバーの役割をあまり説明しませんでした。なので、ここでごく簡単な説明を入れます。

先ほどの図で、KDCやticket、keytabといった見慣れない単語が出てきたと思います。これらはKerberos認証と呼ばれる認証方式を実現するのに必要なものです。

Kerberos認証は双方向認証(クライアント認証+サーバー認証)です。かの有名なActiveDirectoryでもKerberos認証を使うことができます。Kerberosは認証しかしないので、認証対象のユーザーの情報の保存は別に考えなくてはいけません。

KDCはKey Distribution Centerの略で、AS(Authentication Server)とTGS(Ticket Granting Server)から成ります。クライアントはASに認証してもらった後、TGSからticketを受け取り、それを相手に提示することで自分の身分を証明します。keytabは、逆に相手が身分を証明するために用いるものです。このようにして、お互いに身分を証明し合うことにより、安心して通信を行うことができます。

サーバーにKerberos認証を使ってアクセスする際の、認証の詳しい手順は以下の通りです。

## 暗号鍵の生成

パスワードを使う場合

1. ユーザーがユーザー名と所属realm、パスワードを入力します。
2. クライアントプログラムがそれらの情報(credential)から共通鍵を生成します。

keytabを使う場合

1. ユーザーはあらかじめcredentialをクライアントプログラムに登録しておきます。クライアントプログラムはそこから共通鍵を生成し、keytabというファイルに保持します。keytabからcredentialを復元することはできません。

この時点で、クライアントはASとの通信に使う共通鍵を持っています。

## クライアント認証

ASがクライアントが本人であるか確認します。

1. クライアントはASにユーザーIDを送り、認証を要求します。
2. ASはデータベース上に送られてきたユーザーIDに対応するユーザーがいることを確認し、データベース上からcredentialを取ってきて共通鍵を生成します。そして、以下の二つのメッセージをクライアントに返します。
3. <ul><li>A: Client/TGS Session Key [共通鍵で暗号化]</li><li>B: TGT(Ticket Getting Ticket) [TGSの暗号鍵で暗号化]</li></ul>TGTにはユーザーID、ユーザーのネットワークアドレス、チケットの有効期間、Client/TGS Session Keyが含まれます。

3. クライアントはメッセージAを共通鍵で復号し、Client/TGS Session Keyを取り出します。

この時点で、クライアントは以下の二つの情報を持っています。

- Client/TGS Session Key
- TGT [TGSの暗号鍵で暗号化]

## クライアント認可

TGSがクライアントにサーバーへのアクセス権(券)を与えます。

1. クライアントはTGSにサーバーへのアクセス権を要求します。そのさい、以下の二つをTGSに渡します。
 - メッセージC: TGSの暗号鍵で暗号化されたTGTおよびアクセスを要求するサービスのID
 - メッセージD: ユーザーIDおよびタイムスタンプ [Client/TGS Session Keyで暗号化]

2. TGSはメッセージCからTGSの暗号鍵で暗号化されたTGTを取り出して復号し、TGTを得ます。そしてTGSから、Client/TGS Session Keyを取り出します。この鍵を使って、メッセージDを復号し、そこに含まれるユーザーIDとTGTから得られるユーザーIDが一致するか確認します。一致した場合、次の二つのメッセージを返します。
<ul><li> メッセージE: ticket [サーバーの暗号鍵で暗号化]</li><li> メッセージF: Client/Server Session Key [Client/TGS Session Keyで暗号化]</li></ul>ticketには、ユーザーID、ユーザーネットワークアドレス、有効期間、そしてClient/Server Session Keyが含まれます。

3. クライアントはメッセージFを復号して、Client/Server Session Keyを得ます。

この時点でクライアントは、以下の二つの情報を持っています。

- ticket [サーバーの暗号鍵で暗号化]
- Client/Server Session Key

## サーバーへの接続

1. クライアントはサーバーに接続を要求します。そのさい、以下の二つのメッセージを渡します。
 - メッセージE: ticket [サーバーの暗号鍵で暗号化]
 - メッセージG: ユーザーIDとタイムスタンプ [Client/Server Session Keyで暗号化]

2. サーバーは事前に用意されたkeytabから自身の暗号鍵を取り出し、メッセージEを復号して、ticketを得る。さらに、ticketからClient/Server Session Keyを取り出し、メッセージGを復号することで、ユーザーIDを得る。このユーザーIDとticketに含まれるユーザーIDが一致するか調べ、一致する場合は次のメッセージを返す。
 - メッセージH: メッセージGから取り出したタイムスタンプ [Client/Server Session Keyで暗号化]

3. クライアントはメッセージHを復号し、タイムスタンプが送ったものと一致するか確認する。一致した場合は、サーバーに接続する。

以上です。この内容は、[wikipedia](https://en.wikipedia.org/wiki/Kerberos_(protocol))に書いてあります。

クライアントがどうして偽れないのか、サーバーがどうして偽れないのかを考えてみると良いかと思います。

# 前提

[前回記事](https://qiita.com/kanatatsu64/items/ec50707002c5dbd7330d)でやったことを前提とします。

- krb5-admin-server(adminサーバー)が動いている
- krb5-kdc(master KDCサーバー)が動いている
- 管理者用principalがすでに存在する
- firewallが適切に設定されている

前回はKerberosのバックグラウンドにOpenLDAPを使いましたが、今回の内容はバックグラウンドに関わらず使えると思います。

# principalの追加

前回の記事では、管理者principalの追加までやったので、これからはkadmin.localではなくkadminを使っていきましょう。

一般ユーザー用principal(usagi)を追加します。名前のつけ方にはルールがあって、サービスの場合は、`<service-name>/<hostname>@<realm>`に従ってください。ユーザー用principalの場合は、`<user-name>@<realm>`です。`@<realm>`は省略可能で、省略した場合、デフォルトrealmが使われます。

```shell
sudo kadmin -p ${admin}/admin -q 'addprinc usagi'
```

これまではkadminを対話モードで使っていましたが、このように対話モードに入らなくても設定ができます。

サービス用principal(http/www)を追加します。これは、サービスをホストする機器で行います。当然、その機器にはkadminが適切にインストールされている必要があります。前回の記事が参考になるかもしれません。

```shell
sudo kadmin -p ${admin}/admin
kadmin: addprinc http/www -randkey
kadmin: ktadd -k ${service keytab file} usagi # keytabを作る
kadmin: exit
```

一般ユーザーのkeytabも作りたくなるかもしれませんが、ktaddで作ることはできません。ktaddでkeytabを作ると、以前のパスワードは使えなくなるからです。

> Each principal’s keys are randomized in the process. 
(https://web.mit.edu/kerberos/krb5-1.12/doc/admin/admin_commands/kadmin_local.html より)

その代わり、ktutilというコマンドで作ります。

```shell
sudo ktutil
ktutil: read_kt /etc/krb5.keytab
ktutil: add_entry add_entry -password -p ${princ} -k ${kvn} -e ${encry}
ktutil: write_kt /etc/krb5.keytab
```
`/etc/krb5.keytab`というのはデフォルトのkeytabファイルで、`kadmin -k`で使われるものです。もちろん、他のファイルを指定しても構いません。

# principalのパスワードの変え方

これはmiscですが、大事なので書いておきます。パスワードの変更は、

```shell
kpasswd ${principal}
```

で出来ます。ただ、この方法は変更前のパスワードを知っている必要があるので、パスワードを忘れた場合には使えません。そのような時は、

```shell
kadmin -p ${admin}/admin -q ${princ}
```

を使います。元のパスワードを知らなくても変更できるので、忘れた時などにも使えます。

# SSHでKerberos認証を使う

では最後に、実際にkerberos認証を使ってみます。

## sshd_configの設定

まずは、sshサーバーのdaemonであるsshdの設定ファイルsshd_configを変更します。 

```:sshd_config
...
PasswordAuthentication no
...
GSSAPIAuthentication yes
...
```

としてください。

```shell
systemctl restart sshd
```

でdaemonの再起動を行います。

## SSHサーバー用principalの作成

```shell
kadmin -p ${admin}/admin@${REALM}
# sshログインユーザー用principal
kadmin: add_principal ${username}@${REALM}
# sshサーバー用principal
kadmin: add_principal -nokey host/${FQDN}@${REALM}
```

サーバー用principalについては、あとでktaddによって暗号鍵を生成するので、ここではnokeyとしています。

ここで、注意しないといけないのが、sshサーバー用principalの名前です。

上で見た通り、sshクライアントはticketをもらう時に、sshサーバーprincipalを知っておく必要があります。sshクライアントは、おそらく、

```shell
ssh ${FQDN}
```

と要求された場合、sshサーバーのprincipalとしてhost/\${FQDN}@\${default realm}を想定するのでしょう。そのため、sshサーバー用principalの名前は自由に決められません。(要検証)

また\${FQDN}は、SSHサーバー上で

```shell
hostname --fqdn
```

を実行したときの結果と一致している必要があります。サーバーのFQDNを変更するには、`/etc/hosts`ファイルを変更すれば良いです。詳しいやり方は`man hostname`を見るなり、ググるなりして、調べて見てください。

蛇足ですが、sshサーバーがどうやってログインユーザーを決めているかというと、ticketにユーザーIDが含まれるので、そのIDを用いてログインユーザーを決定しているのでしょう。(要検証) ただ、ユーザーIDとuidのバインディングを設定するファイルは見当たりませんでした。どうやって、バインディングしているのかは不明です(ユーザー名?)。ご存知の方はコメントをお願いします。

## keytabの登録

SSHサーバーで、

```shell
kadmin -p ${admin}/admin@${REALM} -q ktadd host/${FQDN}@${REALM}
```

を実行してください。サーバーにkeytabが登録されます。

## SSHクライアントの設定

~/.ssh/configファイルに、以下を追記します。

```~/.ssh/config
Host ${target name}
    HostName ${FQDN}
    User ${login name}
    KerberosAuthentication yes
    KerberosTicketCleanup yes
    GSSAPIAuthentication yes
    GSSAPICleanupCredentials yes
```

ここで、principalの設定がないことに気づきます。が、これできちんと動きます。理由は、[余談 複数のprincipalを使い分ける](複数のprincipalを使い分ける)を見てください。

## TGT(Ticket Granting Ticket)の取得

TGTを取得するには、以下のコマンドを実行してください。

```shell
kinit ${princ name}@${REALM}
```

取得したTGT(とその他のticket)は、

```shell
klist
```
で確認できます。TGTは、

```
krbtgt/${REALM}@${REALM}
```

と表示されるはずです。

## ~/.k5loginの編集

名前が\${login name}@\${default realm}のprincipalは特別な設定なくログインできます。そうでない場合(以下の条件を満たす場合)は設定が必要です。

- `kinit`で指定した\${princ name}が`~/.ssh/conf`で設定した\${login name}と一致しない場合
- `kinit`で指定した\${REALM}がsshサーバーのデフォルトrealmでない場合

この二つの条件のうち一つでも当てはまる場合は、sshサーバー上のユーザー\${login name}のホームディレクトリに.k5loginファイルを作り、以下の内容を書き込んでください。

```~/.k5login
${princ name}@{REALM}
```

また、.k5loginファイルを作成した場合、デフォルトの設定が無効となりますので、名前が\${login name}@\${default realm}のprincipalも、そのprincipalでログインしたいならば.k5loginファイルに書き込む必要があります。

>With no .k5login file, or with k5login_authoritative* set to false, a default rule would permit the principal alice [\${princ name}] in the machine’s default realm to access the alice [\${login name}] account.
</br>
* k5login_authoritativeはkrb5.confファイルで設定できます。
(http://web.mit.edu/Kerberos/krb5-devel/doc/user/user_config/k5login.html より引用)

## 接続確認

```shell
ssh ${target name}
```

パスワードを要求されずに接続できれば成功です。

# 余談
## 複数のprincipalを使い分ける

あるprincipalのTGTをもらうには、

```shell
kinit ${princ}
```

とします。この時、別のprincipalのTGTを

```shell
kinit ${another princ}
```

で要求すると、先ほどもらったTGTが上書きされます。このことは、

```shell
klist
```

が常に最大一つまでのkrb/\${REALM}@\${REALM}チケットしか持たないことから推察できます。

よって、一人のユーザーが持ちうるTGTは(kinitを使う限りは)一つだけであり、そのため、ssh接続するときに使用するprincipalを指定する必要がないのです。

## SSHの再インストール (SSH1を使いたい人向け)

<重要> `ssh kerberos`と検索したときに上位に出てくるhttps://www.oreilly.com/library/view/linux-security-cookbook/0596003919/ch04s14.html やhttps://nnc3.com/mags/Networking2/ssh/ch11_04.htm には、SSHを--with-kerberos5付きでコンパイルするよう書いてありますが、不要です。SSH1でKerberos認証を行うときには、サーバーで

```:/etc/sshd_conf
KerberosAuthentication yes
```
という設定をします。この設定を使うには--with-kerberos5が必要なのでしょうが、SSH2ではその代わりに

```:/etc/sshd_conf
GSSAPIAuthentication yes
```

という設定が使えます。そして、これでもKerberos(を使った)認証ができます。この方法は、少なくとも僕の環境ではデフォルトで使えました。

とはいえ、僕はそれに気付かずにSSHの再インストールを行なったのでその時のメモは以下に残しておきます。

標準でついてくるsshやsshdはKerberos付きでコンパイルされていないことが多いため、自分でコンパイルしたものを代わりに使う必要があります。

```shell
# 依存するライブラリ等のインストール
sudo apt install gcc zlib1g-dev libssl-dev libpam0g-dev libkrb5-dev
# ソースコードのダウンロード
wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-7.9p1.tar.gz
# 解凍
tar xf openssh-7.9p1.tar.gz
```

最新のソースコードは、https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/ とかで配布されています。必ずportableの物を使ってください。

コンパイルして、インストールします。

```shell
cd openssh-7.9p1
# autoconf
./configure --with-kerberos5 --with-pam
make

# 既存のプログラムを必要なら消す
sudo rm /usr/bin/ssh* /usr/sbin/sshd

sudo make install

# systemd unitファイルの書き換え
sudo -e /lib/systemd/system/ssh.service
```