LinuxのCUI操作によるSSH接続

当記事では、LinuxにおいてCUI操作でSSH接続をするときの仕様とか操作について解説します。

## はじめに

LPI-Japanが管理する資格「LinuCレベル1 Version 10.0」受験のために勉強する過程でSSH接続について調べました。当記事では調べた内容をまとめています。

## SSHとは

「Secure Shell」（SSH）はリモート操作で用いられる通信規格の一種です。

旧来の（いくつかの）リモート操作用通信規格とは異なり通信内容を暗号化できるため、盗聴されても通信内容がすぐにバレることを防げます（盗聴自体を防ぐ仕組みはSSHにはありません）。さらに、リモート操作において接続する側（クライアント、ゲスト）と接続される側（サーバー、ホスト）の「なりすまし」を防ぐ認証機能も搭載している充実っぷりで、当記事執筆時点においてはリモート操作といえばSSHという風潮が形成されているように感じられます。

SSHを使ったリモート操作の具体的な流れとしては以下のようになります。

1. SSH通信が可能かの確認
2. 通信内容の暗号化
3. ホスト認証（サーバー認証とも）
4. ユーザー認証
5. 完全に接続

### OpenSSH

SSHは規格であり、この規格をプログラムに落とし込む必要があります。

いくつかの実装がありますが、なかでも有名なのが「OpenSSH」です。少なくとも、LinuxにおけるCUI操作でのSSH通信といえばOpenSSHの使用が真っ先に候補に挙がるでしょう。当記事ではOpenSSHの操作用コマンド `ssh` の使い方を解説していきます。

## そもそも暗号化とは

本題に突入する前に、そもそも暗号化とは何なのかについて私の認識を共有させてください。

多くの生物はさまざまな方法で情報をやり取りします。より具体的に言えば「媒体」と「規則」に基づいて情報をやり取りします。例えば「会話」は「空気（の振動）」という媒体と「言語」という規則を組み合わせています。そして、左記のように「媒体」と「規則」を使って何らかの情報をやり取りすることを「通信」と言います。

通信の内容を知るためには通信媒体に干渉出来ることに加えて通信規則を理解しておく必要もあります。通信というのは必ず複数人で行うものですから、通信対象に含まれる全員が（ここ重要）通信媒体と通信規則を理解できる状態でなくてはなりません。それは往々にして手間のかかることですが、逆に言えば通信媒体に干渉できて通信規則を理解している人であれば想定している通信対象以外でも通信内容を把握することができます。これがいわゆる「盗聴」になります。

盗聴を回避する最善の策は、そもそも通信が行われていること自体を秘匿することですが、相手が本気で盗聴を試みているならば通信内容は何らかのかたちで盗聴されるものと考えるべきです。そんな状況で役立つのが通信内容の「暗号化」です。

暗号化とは、元々の通信内容「平文」を任意の「暗号化方式」（暗号化アルゴリズムとも）によって別の通信内容「暗号」に置き換えることです。暗号を平文に戻すことを「復号」と言います。

また、暗号化方式には「鍵」と呼ばれる値が関わってくることがしばしばあります。暗号化方式を関数とするならば鍵は引数のようなものです。

有名な暗号化方式としてシーザー暗号（シフト暗号とも）があります。これは平文に含まれるアルファベットを任意の方向に、任意の数だけずらすというものです。ここでいう「ずらす方向」と「ずらす量」がシーザー暗号における鍵ということになります。疑似コードで表現すると以下のようになります。

```javascript
/**
 * 平文をシーザー暗号で暗号化します。
 * @param {string} plaintext 暗号化の対象となる平文です。
 * @param {string} key1 ずらす方向です。正順（AからBへ）は "f" 、逆順（BからAへ）は "b" を指定してください。
 * @param {number} key2 ずらす量です。
 */
function encryptionByCaesarCipher(plaintext, key1, key2) { ... }

// "BCD"
console.log(encryptionByCaesarCipher("ABC", "f", 1));

// "ZAB"
console.log(encryptionByCaesarCipher("ABC", "b", 1));

// "ROVVY GYBVN"
console.log(encryptionByCaesarCipher("HELLO WORLD", "f", 10));
```

## 鍵配送問題

通信内容を平文ではなく暗号にすることで、通信内容が意図しない第三者に漏れてもすぐに通信内容がバレるという事態を防ぐことができます。ただ、絶対にバレないという保証はありません。暗号の歴史というのは非常に長く、そのなかでさまざまな暗号化方式が研究されてきました。そして、暗号化の方式が研究されてきたということは暗号化方式の傾向や分類もできているということで――つまるところ、ある程度当たりを付けて復号することもできなくはないということです。とはいえ、そのようなことをするには相応に時間・労力・費用がかかりますので、見合うだけのリターンがあると確信できていなければ諦めるのが普通でしょう。これが暗号化の利点です。

そんなことよりも、暗号化において問題になるのが暗号化に用いた暗号化方式と鍵をどのようにして通信対象と共有するのかという問題です。言わずもがな、暗号を解くには暗号化方式と鍵を通信相手が知っていないといけないわけですが、そのことを通信しようとすると、その通信内容を盗聴されてしまう危険性があります。このジレンマを「鍵配送問題」と言います

長らく、この鍵配送問題は暗号を使う以上避けては通れない問題だと考えられてきました。かなりの費用をかけて極めて慎重に暗号化方式と鍵を共有するか、あるいはある程度盗聴される可能性を考慮に入れてコスパ重視で共有するかを迫られてきました。

これを解決したのが1976年に登場した「ディフィー・ヘルマン鍵共有」（ディフィー・ヘルマン鍵交換）という
仕組みです。略して「DH法」と呼ばれます。

## DH法

DH法は各通信対象が内々で秘匿しておく「秘密鍵」と通信対象と共有する「公開鍵」という2つの鍵を用いて、暗号化方式で使う鍵（以後「共通鍵」と表記します）を通信対象間で安全に共有する方法です。

論より証拠ということで実際にDH法を使って暗号化通信する場合の状況を考えてみます。DH法にはいくつかの種類があるっぽいのですが、当記事ではそのうちの1つを使っていきます。

### DH法の実演

通信対象はAさんとBさんの2人とします。

AさんとBさんは通信開始前に以下のことを共有しておきます。このことは外部に漏れても構いません。もちろん知られないのが最善です。

+ 通信内容の暗号化方式
+ 通信内容の暗号化方式で使う鍵はDH法で共有すること
+ 公開鍵1
+ 公開鍵2

以上のことを通信対象と共有します。公開鍵1はできるだけ大きな素数、公開鍵2は「1」よりも大きく公開鍵1よりも小さい自然数にします。ここでは仮に、暗号化方式はシーザー暗号（AからZ方向にずらす）、公開鍵1を「53」、公開鍵2を「2」こととしましょう。

また、通信開始前に、各々が適当な自然数を考えておきます。Aさんは「5」、Bさんは「8」とします。この数字が秘密鍵になります。秘密鍵は絶対に外部に漏れてはいけません。

では始めます。まずは以下のように計算してください。

```
1. 公開鍵2を自信の秘密鍵で累乗する。
   A: 2 ^ 5 = 32
   B: 2 ^ 8 = 256

2. 計算結果を公開鍵1で割った余りを求める。
   A: 32 mod 53 = 32
   B: 256 mod 53 = 44
```

Aさんは「32」、Bさんは「44」になりました。この値を相手に送信します。この値は外部に漏れても大丈夫です。

送信されてきた値を受けとった両者はさらに以下のように計算します。

```
1. 相手から送られてきた値を自信の秘密鍵で累乗する。
   A: 44 ^ 5 = 164916224
   B: 32 ^ 8 = 1099511627776

2. 計算結果を公開鍵1で割った余りを求める。
   A: 164916224 mod 53 = 46
   B: 1099511627776 mod 53 = 46
```

お互いに「46」という値が出ましたね。これが共通鍵になります。通信上には「46」という値は流れていないのに、お互いが同じ鍵を算出できる。この天才的な仕組みがDH法になります。なお、言うまでもありませんがこうして求めた共通鍵を通信上に流してはいけません。以後、お互いは相手が自身と同じ共通鍵を算出したという前提に立って、あらかじめ共有しておいた暗号化方式に則って通信を行います。

とはいえ、この方法も完全無欠というわけではなく時間をかければ共通鍵を求めることが分かっています。ただ、それにかかる時間があまりに膨大であることから現実的には復号は不可能とされています。

## SSH通信の開始とSSH通信可能かの確認

SSH通信を開始する場合はコマンド `ssh` を用います。

```
# ssh [<オプション>] <接続したいユーザー名>@<接続したいサーバー名>
```

接続したいユーザー名は `-l` オプションでも選択できます。また、ユーザー名を省略した場合は自身と同じ名前のユーザー名で接続を試みます。

```
# ssh example_user@example_server
```

```
# ssh -l example_user example_server
```

接続する側（便宜上「SSHクライアント」とする）が接続される側（便宜上「SSHサーバー」とする）に対してSSH通信を試みようとするとき、最初に行われるのがSSHによる通信が可能であるのかの確認です。

SSHで使われる暗号化アルゴリズムは1種類ではなく、いくつかのアルゴリズムがあります。SSH通信を行うときは、まずはSSHクライアントとSSHサーバーの間でどのアルゴリズムを使うかを打ち合わせます。

ここでお互いに共通のアルゴリズムが使えないことが判明したら通信を諦めます。

## 通信内容の暗号化

お互いにSSH通信ができると分かったらDH法（およびDH法から派生した仕組み）を使って共通鍵を共有します。

以後のやり取りは通信は共通鍵で暗号化されて行われます。

## サーバー認証

SSH通信できることが分かった。DH法で暗号化のための共通鍵も共有した。じゃあ、ここからは普通に通信を――とはならないのがSSHです。

通信内容の暗号化に成功したら、次は「サーバー認証」（ホスト認証とも）を行います。今から通信しようとしているサーバーが過去に接続したことのあるサーバーと同一であるかを確認するわけです。この仕組みがあるおかげでサーバーのなりすましを事前に探知することができます。

### ホスト鍵

SSHサーバーには「ホスト鍵」と呼ばれる全てのSSHサーバーにおいて基本的に一意となる値が必ず用意されています（超低確率でどこかのSSHサーバーと同じ値が出てしまう可能性があるみたいです）。厳密には「ホスト秘密鍵」「ホスト公開鍵」の2つがあり、ホスト秘密鍵は絶対に外部に漏出してはいけない鍵、ホスト公開鍵は自身への接続を希望する全てのSSHクライアントに配布する鍵です。

ホスト鍵はSSHサーバーアプリをインストールしたときにディレクトリ `/etc/ssh/` の直下に自動的に生成されます。ホスト鍵の再生成は可能ですが不必要に再生成するのは避けるべきです。前述のとおり、ホスト鍵が変わっていると別のサーバーであるとOpenSSHがSSHクライアントに警告を発してしまうためです。

SSHクライアントがSSHサーバーに接続しようとするとき、SSHサーバーに対してホスト鍵を使った電子署名の作成と送信を要求します。送られてきた電子署名を事前に入手していたホスト公開鍵を使って処理することで、ホスト公開鍵の持ち主と電子署名の作成主が同じサーバーであるかを判定します。

なお、初回接続時はホスト公開鍵を持っていないためアプリ側から「過去に接続が許可されたSSHサーバーじゃないけど本当に接続しますか？」と聞かれます。ここで許可すると、そのSSHサーバーの情報がSSHクライアントのホームディレクトリの `~/.ssh/knowns_host` に登録されて次回から聞かれなくなります。また、2回目以降の接続時には `~/.ssh/knowns_host` に使われている値でサーバー認証をしますので、どこかのサーバーがなりすましを行っている――サーバーのホスト鍵が別のものになっていた場合は「過去に接続が許可されたSSHサーバーじゃないけど本当に接続しますか？」と聞いてくれます。

ちなみに、これは私も勘違いしていたことなのですがサーバー認証は「過去に接続したことのあるSSHサーバーと同じサーバーであるか？」を判定する仕組みであり「そのサーバーが意図したサーバーであるか？」を判定する仕組みではありません。つまり、初回接続時の段階からSSHサーバーがなりすましだった場合はずっと騙され続けることになります。

これを回避するには、事前に接続したいSSHサーバーの管理者から信頼できる手段でホスト公開鍵を受け取って `~/.ssh/knowns_host` に登録しておくなどするしか回避できないと思われます。

## ユーザー認証

サーバー認証が終わると今度は「ユーザー認証」があります。これは要するにログイン処理です。

SSHクライアントはSSHサーバーに接続するとき、SSHサーバーが動いているLinux端末上に存在しているいずれかのユーザーでログインすることになります。そのため、そのユーザーの名前とパスワードを知っている必要があります。名前とパスワードを正しく入力できればSSH接続開始。間違えたらSSH接続は切断されます。

なお、上記の方法はSSHのユーザー認証の仕組みの1つである「パスワード認証」と呼ばれるもので、さまざまな理由からあまり推奨されていません。一部のサービスではユーザー認証でパスワード認証の使用ができなくなっているものもあります。

現在主流なのは「公開鍵認証」と呼ばれるものです。SSHサーバーにホスト鍵があるように、SSHクライアントもSSH接続前に秘密鍵・公開鍵を生成しておいて、接続したいSSHサーバーに公開鍵を送信しておくことでユーザー認証を通ることができるようになるのです。

仕組み自体はサーバー認証と同じで、ユーザー認証時にSSHクライアントから公開鍵認証によるユーザー認証を要求されたときSSHサーバーはSSHクライアントに公開鍵の提出を要求するとともに、提出された公開鍵とこれまでにSSHクライアントから送信されてきた公開鍵の情報をまとめたファイル `~/.ssh/authorized_keys` の情報を使ってSSHクライアントがなりすましていないかを判定しています。

公開鍵接続時だけはログインするユーザーのパスワード入力が要求されますが、それ以降はユーザー名さえ選択していれば自動的にログインしてくれます。

ユーザー認証で問題なければようやくSSHによるリモート操作の開始です。

## ユーザー認証用鍵の生成や送信

ユーザー認証の1つである公開鍵認証を行うための、SSHクライアント用秘密鍵・公開鍵を生成して送付する手順は以下のとおりです。

### 鍵の生成

SSHクライアントの公開鍵認証用鍵を生成するにはコマンド `ssh-keygen` を使用します。

```
# ssh-keygen [<オプション>]
```

生成された鍵はディレクトリ `~/.ssh/` の直下に `id_rsa` や `id_rsa.pub` といった名前で保存されます。ファイル名末尾に `.pub` という名前が付いているのが公開鍵、付いていないのが秘密鍵です。

### 鍵の送付・登録

生成した秘密鍵・公開鍵のうち、公開鍵はSSHサーバーに送付して登録してもらう必要があります。

ここでいう登録とは、SSH通信によってリモート操作するユーザーのファイル `~/.ssh/authorized_keys` に公開鍵のファイルの中身を追記することを指しています。左記ファイルにSSHクライアントから送られてきた公開鍵の情報がまとめられています。

一番手っ取り早いのはコマンド `ssh-copy-id` です。宛先を指定すれば自身が持っている全ての公開鍵を送信・登録してくれます。特定の公開鍵だけ送りたいという場合は `-i` オプションを付けて、送信したい公開鍵のパスを記述します。

```
# ssh-copy-id [<オプション>] <送信先ユーザー名>@<送信先アドレス>
```

```
# ssh-copy-id -i <送信したい公開鍵のパス> <送信先ユーザー名>@<送信先アドレス>
```

コマンド `ssh-copy-id` が使えない場合や、SSHサーバーの管理者などから「公開鍵の登録はこちらでやりますので公開鍵の送付だけお願いします」などと言われた場合はコマンド `scp` などで公開鍵の送信だけ行うことができます。

```
# scp <送信したい公開鍵のパス> <送信先ユーザー名>@<送信先アドレス>:<送信先のパス>
```

コマンドの文法はちょっとややこしいのですが「192.168.0.1」の「exampleuser」さんのホームディレクトリ直下のディレクトリ「keys」に送信したい場合は以下のように記述します。

```
# scp ~/.ssh/id_rsa.pub exampleuser@192.168.0.1:~/keys
```

公開鍵を受けとったSSHサーバー側は以下コマンドなどを使って、ファイル `~/.ssh/authorized_keys` に送られてきたファイルの内容を追記します。公開鍵はテキストなので適当なコマンドで追記すればOKです。

```
# cat ~/id_rsa.pub >> ~/.ssh/authorized_keys
```

## おわりに

以上がLinuxにおけるCUI操作でのSSH接続の概要です。他にも秘密鍵に付ける「パスフレーズ」や、パスフレーズによる確認を省略する「認証エージェント」といったものもあるのですが割愛します。

興味が湧いた方は各自で調べてみてください。