GCE ルートと権限を考えてVMインスタンスにSSH接続する

この記事は「[Google Cloud Platform Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp)」1日目の記事です。


GCPのVMインスタンスにSSH接続する手段を、適切な役割(権限), 適切なファイアウォールルール, その他適切な何かを紹介することを目的としています。
想定している読者は次のような方です。

+ `default` VPCネットワークを利用している
+ SSH接続するためにインスタンスに外部IPアドレスが必要だと思っている
+ `--tunnel-through-iap`という文字列を初めて見る
+ `--internal-ip`という文字列を初めて見る
+ 企業に勤めていてアクセス制御を考えなければならない

逆に次のような方は読む必要がありません。
ちなみに私はこちらに該当します。

+ 俺たちはコンテナをCloud Run(Fully managed)にデプロイしてServerless NEG, VPC Accessと併用するのが最強だと思っているしそうしている
+ 俺たちはコンテナをKubernetesクラスタにデプロイするのが最強だと思っているしそうしているし、ホストマシンに興味がない
+ Cloud ConsoleにおけるCompute Engine Vm instancesというメニューに興味がない
+ 俺たちは全員がOwner/Editor役割でGCPを利用している

GCPにおける「役割」と「権限」の意味について理解してから読むことをオススメします。
文中の`<>`記号で囲まれた文字列は実行環境において別の文字列に置換されることを期待しています。

## VMインスタンスに接続する手段のまとめ

役割やファイアウォールの前に、まずは接続する手段を整理しましょう。
といっても、大きく2通りしかありません。

### 任意のsshクライアントで接続する

今までと同じように任意のsshクライアントを利用して接続することができます。
ssh接続をしたいユーザから公開鍵をもらってインスタンスにアップロードするわけですが、GCPではインスタンスのmetadataに公開鍵を書くのが便利です。
terraformでやろうとすると辛い思いをするやつですね。
インスタンスにはkey-value形式で任意のメタデータを設定することができますが、公開鍵を登録する場合のkeyは`ssh-keys`です。
valueにはユーザの公開鍵を改行区切りで記述します。
Cloud Consoleから作成した方が専用のメニューがあって遥かに楽なのでそうしましょう。
参考までに該当メニューを切り抜いておきます。
***
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121808/97a30b3a-cfb7-72f6-deaf-778d0565a5d8.png)
***

### gcloud compute ssh コマンドで接続する

たぶんこれが一番楽だと思いますが、この方法の欠点は`gcloud`コマンドがインストールされていて、SSH接続するに足る権限を持ったGoogleアカウントかサービスアカウントが必要だという点です。

## 外部から接続する際の安全で楽なファイアウォールルールを設定する

`sshd`がtcp:22で動いていると仮定すれば、当然該当するallowルールが必要です。
GCPでプロジェクト作成時に最初から存在している`default` VPC ネットワークにおいては、tcp:22が`0.0.0.0/0`からallowと設定されているので、比較的危険な部類に入るでしょう。

`default` VPC ネットワークには他にもガバガバなルールが設定されているので、GCPを勉強中の方も業務で利用する方も、まずはこのネットワークを削除して、自分でVPCネットワークや関連するサブネットワークなどを設定するようにしましょう。

ところで、これを読んでいるみなさんはきっと在宅勤務をしている方がほとんどだと思います。
在宅勤務は自分たちの組織のファイアウォールルールを見直すきっかけになったはずですが、GCPではどのように設定するのが**最も**安全かつ楽なのか、少しだけ考えてみましょう。
あなたの所属する組織はGoogle WorkspaceまたはCloud Identityを導入しているものとします。

はい、次のような手段が考えられますね。
Google Cloudの認定試験のつもりになって考えましょう。

1. メンバーの自宅IPアドレスを提出してもらって、全てに対してallowルールを設定する
+ 公開鍵を登録したユーザまたはGoogleアカウントを持っているユーザからしか接続できないので、ネットワーク上の全てのインスタンスに対して`0.0.0.0/0`からのallowルールを設定する
+ 社内ネットワークへのVPNと、そのネットワーク内に踏み台マシンを用意する。踏み台マシンの外部IPアドレスに対してallowルールを設定する
+ GCPに踏み台インスタンスを作成し、踏み台インスタンスに対してのみ有効な`0.0.0.0/0`からのtcp:22を許可するallowルールを設定する

みなさんの考えの中に同じものは...ないはずですね。
3番だけは組織のポリシーによってはありそうですが、Google Cloud認定試験的にはきっと不正解ですし、必須ではありません。

正解は、「`35.235.240.0/20`からのtcp:22を許可するファイアウォールルールを作成し、ユーザの`gcloud compute ssh`コマンドに`--tunnel-through-iap`オプションを付与してもらう」、のが一例になります。

この`35.235.240.0/20`はIdentity Aware Proxyといって、よくIAPと略されます。
IAPは、ユーザがSSHする権限を持っているかどうかを検証してくれるため、ここを通ってSSHを試みれば、該当のインスタンスに対して `IAP-secured Tunnel User`役割を持つGoogleアカウントのみが接続できることになります。

IAPに関するこれ以上の詳しい説明は長くなってしまうので、一連のことが書いてある[公式ドキュメント](https://cloud.google.com/iap/docs/using-tcp-forwarding#preparing_your_project_for_tcp_forwarding)に説明を任せることにします。

加えて重要なことは、IAPを介するのであれば、インスタンスに外部IPアドレスは必要無いという点です。
GCPでは外部IPアドレスに対して若干の課金が発生するため、不要な外部IPアドレスは節約しましょう。

もしユーザがGoogleアカウントを持っていなくても、サービスアカウントを渡せば同様に認証/認可が可能ですが、ユーザにCloud SDKの利用を強制できないのであれば、従来どおりのIP制御で行ってください。きっとそういう組織には社内踏み台用マシンがあるはずです。

## GCP内部に作成した踏み台インスタンスから他のインスタンスへSSH接続する

様々な事情で、VPCネットワーク内部に踏み台ホストを用意するパターンがあります。
踏み台インスタンスの名前を`step`、アプリケーションが実行されるインスタンスを`application`として、`step`にSSH接続してから`application`に接続するパターンを考えます。

まずは`step`にSSH接続をしましょう。

```bash
$ gcloud beta compute ssh --zone "asia-northeast1-b" "step" \
  --tunnel-through-iap --project "<YOUR_PROJECT_ID>"

<user>@step:~$ gcloud auth list
                  Credentialed Accounts
ACTIVE  ACCOUNT
*       924448380232-compute@developer.gserviceaccount.com
```

インスタンスには特に何も指定しなければ、Compute Engine default Service Accountが設定されています。
これは少し特殊なアカウントなのですが、ともかくインスタンスにSSHするための権限を持っていないので、このままでは権限不足で他のインスタンスにSSH接続することができません。
これを解決する手段はいくつかあるのですが、基本に忠実に(?)自分のアカウントでログインし直しましょう。

```bash
$ gcloud auth login

...(省略)

<user>@step:~$ gcloud auth list
                  Credentialed Accounts
ACTIVE  ACCOUNT
        924448380232-compute@developer.gserviceaccount.com
*       <YOUR_GOOGLE_ACCOUNT>
```

同じVPCネットワーク内であれば、`--internal-ip`オプションを付与することによって、インスタンスの内部IP宛にSSH接続が可能です。

```bash
$ gcloud beta compute ssh --zone "asia-northeast1-b" "application" \
  --internal-ip --project "<YOUR_PROJECT_ID>"
```

## OSLoginを使ってIAMでSSH接続を制御する

さて、そもそもどうして`gcloud`コマンドでVMインスタンスにSSH接続できるのでしょうか？
それは、`gcloud compute ssh`を実行する際に、プロジェクトのメタデータにユーザの公開鍵が登録され、全てのインスタンスがそのメタデータを参照するためです。
つまり、プロジェクトのメタデータを変更する権限を持ちさえすれば、プロジェクト内の全てのインスタンスにSSH接続することができます。
これを防ぐために、インスタンスにはプロジェクトレベルの公開鍵を参照しないように設定することができますが、その場合はインスタンスに公開鍵を登録しないとSSH接続できないため、少しだけめんどくさいです。

そこで、[OSLogin](https://cloud.google.com/compute/docs/instances/managing-instance-access)という機能を使って、ユーザのGoogleアカウントに対してSSH接続の可否を制御しましょう。
これを利用すると、インスタンスにユーザの公開鍵を登録することなく、ただ1つメタデータを設定するだけで、IAMでSSHの可否を制御することが可能になりとても便利です。

ちなみに、あまり詳しくないことに敢えて触れておくと、Google WorkspaceかCloud Identityで管理されているアカウントの場合は、Admin Consoleで設定しておく必要がある、と思っています。
無効にして試すのはめんどくさいので、この先は君自身の目で確かめてくれ！
以下はAdmin Consoleの管理画面の設定例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121808/02e0c938-3e0f-7c77-10ad-82743c187898.png)


### OSLoginを利用してSSH接続する

OSLoginを利用するためには、まずは踏み台インスタンス`step`にメタデータ**enable-oslogin=TRUE**を設定しましょう。

```bash
$ gcloud compute instances add-metadata step --zone=asia-northeast1-b \
  --metadata=enable-oslogin=TRUE \
  --project="<YOUR_PROJECT_ID>"
```

こうしてから`gcloud compute ssh`すると、何かが変わったことに気が付きます。

```bash
<なんかとても長いユーザ名>@step:~$
```

OSLoginを利用してSSH接続するとユーザ名が`ドメイン_ユーザ名`というように変わります。
この挙動を変更し、ユーザ名のみとすることも噂によるとできるそうです。
上のキャプチャ画像中の、「OS Login APIによって生成されるユーザ名にドメインサフィックスを含める」をオフにすれば良いような気がしますが、試すのはめんどくさいのでこのさk（ry

### OSLoginに関わる役割

OSLoginを利用してSSH接続する際に重要な役割は以下の3つです。

- Compute OS Login
- Compute OS Admin Login
- Service Account User

メタデータを設定したインスタンスにSSH接続するためには、`Compute OS Login`役割が必要です。
さらに、rootユーザを利用する場合には`Compute OS Admin Login`役割が必要です。
また、SSH接続した直後はVMインスタンスに設定されたサービスアカウントを利用することになるため、もしインスタンスにサービスアカウントが設定されている場合にはこれらに加えて`Service Account User`役割が必要です。

これができるようになると、先程の踏み台インスタンスでもOSLoginを利用することを考えられます。
踏み台インスタンスにカスタムなサービスアカウントを設定し、そのサービスアカウントに`OS Login`役割を付与すれば、踏み台から他のインスタンスにOSLoginでSSH接続することができます。

## まとめ

IAPとOSLoginを利用して、楽をしつつ安全なルートかつ適切な権限でSSH接続をしていきましょう。

## おまけ

`gcloud compute ssh`に頼っていると、ansibleなどでホスト名を指定したSSHが必要な場合に少し困ることがあります（ありました）。

まず、`gcloud compute ssh`の裏側を`--dry-run`オプションをつけて暴きましょう。

```bash
$ gcloud beta compute ssh --zone "asia-northeast1-b" "step" \
  --tunnel-through-iap --project "<YOUR_PROJECT_ID>" \
  --dry-run
```

出力は省略しますが、これを踏まえると `$HOME/.ssh/config`に次のように書いておけば、ansible実行においてGCPのインスタンスへSSH接続を行うことができます。

```$HOME/.ssh/config
Host <HOST>
  HostName compute.<GCP_INSTANCE_ID>
  IdentityFile /Users/<LOCAL_USER>/.ssh/google_compute_engine
  CheckHostIP no
  HostKeyAlias compute.<GCP_INSTANCE_ID>
  IdentitiesOnly yes
  StrictHostKeyChecking no
  UserKnownHostsFile /Users/<LOCAL_USER>/.ssh/google_compute_known_hosts
  ProxyCommand /Library/Developer/CommandLineTools/usr/bin/python3 -S /Users/<LOCAL_USER>/Applications/google-cloud-sdk/lib/gcloud.py beta compute start-iap-tunnel <HOST> %p --listen-on-stdin --project=<YOUR_PROJECT_ID>--zone=<INSTANCE_ZONE> --verbosity=warning
  ProxyUsefdpass no
  User <USER>
```