OCI Bastionのセッション作成を自動化するスクリプトを作る

# はじめに

最近、OCIのBastionに触っています。Bastion（日本語で要塞）はサーバーへのアクセスの踏み台になってくれるサービスです。自前で踏み台サーバーを作るのと比べて、プライベートなネットワークで完結し、無料枠であることが利点としてあります。

https://www.oracle.com/jp/security/cloud-security/bastion/

しかし、使っているとTTLの短さやセッション作成の手間が気になってきます。そこで、何とか楽に使えないか考えていきます。

# Bastionの接続方法

Bastionの使い方の詳細はOCIのドキュメントにあります。

https://docs.oracle.com/ja-jp/iaas/Content/Bastion/Tasks/connectingtosessions.htm#connect_to_managed_ssh

概要を記載します。
Bastionを使ったサーバーへの接続方法としては以下の種類があります。

* SSH接続
* ポート転送
* リモートデスクトップ接続

今回はSSH接続を使います。
サーバーへの接続の流れは以下の通りです。

1. OCIコンソールにアクセスする。
1. セッションを作成する。
1. 作成されたセッションIDを確認する。
1. ターミナル（サーバーに接続するためのツール）を開く。
1. セッションIDを使ってサーバーに接続する。
1. TTLが切れたら、再度セッションを作成する。

大変だなーと思いますので、何とか楽にできないか考えます。

# 出来上がり

以下のファイルを作りました。

* セッションを作成するスクリプト
* SSHのconfigファイル

今回のスクリプトを使うと以下のようになります。

1. ターミナルを開く。
1. サーバーに接続する。
1. TTLが切れたら、再度セッションを作成する。

接続のコマンドは以下の1文で済みます。

```bash
$ ssh ＜ホスト名／IPアドレス＞
```

# スクリプトのフロー

作成するスクリプトのフローは以下の通りです。
以下の２点に気を付けました。
* OCIのCLIはインストールしたくないのでREST APIを使ってcurlで実現したい。
* OCIのREST APIでは作成を待機するオプションがないので待機する処理を入れる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/a7224ce1-cecf-500a-e489-f609c82b2a6b.png)

# セッションを作成するスクリプトを作成する

セッションを作成して、そのOCIDを取得するshellファイルは以下の通りです。自分で使う用なので、荒いのは申し訳ないです。。

```bash:get-session-ocid.sh
#!/bin/bash

#必要なパラメータは外部のファイルに書くようにしました。
ENV_FILE=$1
source $ENV_FILE

function create_session(){
  rest_api="/20210331/sessions"
  publicKeyContent=$(cat ${publicKey} | awk '{print substr($0, 1, length($0)-1)}')
  body="{\"bastionId\": \"${bastionId}\", \"displayName\": \"${displayName}\", \"keyDetails\": {\"publicKeyContent\": \"${publicKeyContent}\n\"}, \"sessionTtlInSeconds\": \"${sessionTtlInSeconds}\", \"targetResourceDetails\": {\"sessionType\": \"${sessionType}\", \"targetResourceId\": \"${targetResourceId}\", \"targetResourceOperatingSystemUserName\": \"${targetResourceOperatingSystemUserName}\"}}"
  content_sha256="$(echo "$body" | openssl dgst -binary -sha256 | openssl enc -e -base64)"
  content_sha256_header="x-content-sha256: $content_sha256"
  content_length=$(echo "$body" | wc -c | xargs)
  content_length_header="content-length: $content_length"
  headers="(request-target) date host"
  headers=$headers" x-content-sha256 content-type content-length"
  content_type_header="content-type: application/json"
  now=`date -u "+%a, %d %h %Y %H:%M:%S GMT"`
  date_header="date: $now"
  host_header="host: $bastionHost"
  request_target="(request-target): post $rest_api"
  signing_string="$request_target\n$date_header\n$host_header"
  signing_string="$signing_string\n$content_sha256_header\n$content_type_header\n$content_length_header"
  signature=`printf '%b' "$signing_string" | openssl dgst -sha256 -sign $privateKeyPath | openssl enc -e -base64 | tr -d '\n'`
  response=$(echo "${body}" | curl -X POST --data-binary @- -sS https://$bastionHost$rest_api -H "date: $now" -H "x-content-sha256: $content_sha256" -H "content-type: application/json" -H "content-length: $content_length" -H "Authorization: Signature version=\"1\",keyId=\"$tenancy_ocid/$user_ocid/$fingerprint\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\"")
  echo $response | sed -e 's/^.*"id" : "\([^"]*\)".*$/\1/'
}

function is_session_alive(){
  sessionId=$1
  rest_api="/20210331/sessions/${sessionId}"
  now=`date -u "+%a, %d %h %Y %H:%M:%S GMT"`
  date_header="date: $now"
  host_header="host: $bastionHost"
  request_target="(request-target): get $rest_api"
  signing_string="$request_target\n$date_header\n$host_header"
  headers="(request-target) date host"
  signature=`printf '%b' "$signing_string" | openssl dgst -sha256 -sign $privateKeyPath | openssl enc -e -base64 | tr -d '\n'`
  response=$(curl -X GET -sS https://$bastionHost$rest_api -H "date: $now" -H "Authorization: Signature version=\"1\",keyId=\"$tenancy_ocid/$user_ocid/$fingerprint\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\"")
  lifecycleState=$(echo $response | sed -e 's/^.*"lifecycleState":"\([^"]*\)".*$/\1/')
  echo $lifecycleState >&2
  if [ -n "$lifecycleState" ] && [ "$lifecycleState" = 'ACTIVE' ];then
    echo true
  else
    echo false
  fi
}

sessionId=$(create_session)
until $(is_session_alive $sessionId);do
  sleep 3
done
echo $sessionId
```

以下に参考にしたドキュメントを載せておきます。
* Bastionのリストを取得するAPIのリファレンス
https://docs.public.oneportal.content.oci.oraclecloud.com/en-us/iaas/api/#/en/bastion/20210331/Session/ListSessions
* OCIのREST APIの使用例が載っているサイト
https://www.ateam-oracle.com/post/oracle-cloud-infrastructure-oci-rest-call-walkthrough-with-curl
https://itport.cloud/?p=8745

パラメータを定義したファイルには以下の情報を書きました。

```bash:envfile.sh
#
# セッションに関する情報
#
export displayName="..." #OCIコンソールから見えるセッション名
export publicKey="..." #SSH接続に用いる公開鍵のパス
export sessionType="MANAGED_SSH" #セッションの種類
export targetResourceId="..." #SSH接続するサーバーのOCID
export targetResourceOperatingSystemUserName="..." #SSH接続する際のユーザー名
export sessionTtlInSeconds="..." #セッションのTTL

#
# Bastionに関する情報
#
export bastionId="..." #BastionのOCID
export bastionHost="bastion...." #Bastionのエンドポイント

#
# APIに関する情報
#
export compartmentId="..." #コンパートメントのOCID
export tenancy_ocid="..." #テナンシのOCID
export user_ocid="..." #ユーザーのOCID
export privateKeyPath="..." #APIを使用するための秘密鍵
export fingerprint="..." #鍵のフィンガープリント
```

パラメータについて補足します。
* privateKeyPath
OCIにてAPIを使用するための鍵を登録する必要があります。手順は以下のURLにあります。
https://docs.oracle.com/ja-jp/iaas/Content/API/Concepts/apisigningkey.htm#two
* bastionHost
リージョンごとのエンドポイントは以下のURLに一覧があります。
https://docs.oracle.com/en-us/iaas/api/#/en/bastion/20210331/

# SSHのconfigファイルを作成する

作成したスクリプトを実行するconfigファイルを作成します。
元になる設定はセッションをOCIコンソールから作成したときに見ることができます。以下の構造になっています。

```bash
ssh -i <privateKey> -o ProxyCommand="ssh -i <privateKey> -W %h:%p -p 22 <セッションのOCID>@host.<Bastionのエンドポイント>" -p 22 <ユーザー名>@<サーバーのIPアドレス>
```

上記のコマンドを実行するのは長くて面倒なので、configファイルにします。
以下の通りです。

```config:~/.ssh/config
StrictHostKeyChecking no #面倒なのでOFFにしています。
UserKnownHostsFile=/dev/null #トラブルの原因になったことがあるのでOFFにしています。
ServerAliveInterval 50 #Bastionだと操作しないと切られるので設定します。

Host <ホスト名>
  HostName <サーバーのIPアドレス>
  User <ユーザー名>
  IdentityFile <privateKey>
  ProxyCommand ssh -i <privateKey> -W %h:%p -p 22 $(/path/to/get-session-ocid.sh /path/to/envfile.sh)@host.<Bastionのエンドポイント>
```

無理やりですが、セッションのOCIDをスクリプトから取得するようにしています。

# SSHコマンドの実行

以上で準備はできました。
後は、コマンド１文でサーバーにSSH接続することができます。

```bash
$ ssh ＜ホスト名＞
```

# 苦労した点＆今後の方針

## curlのデバッグオプション

curlを使ってコードを書いているとミスしたときに、原因を特定するのに苦労しました。
デバッグで使用したオプションを記載します。

* v,vvvなどのverboseオプション
リクエストのパスやヘッダーを確認できます。
* traceオプション
今回はデータに改行コードがついていたりついていなかったりで詰まりました。バイナリでデータを表示されるので、どこがおかしいかわかりやすくなります。

※参考
https://qiita.com/yasuhiroki/items/a569d3371a66e365316f

## さらに楽に接続したい

今回作ったものでは以下の点が気になります。

* SSH接続で毎回セッションを作成するので時間がかかる
* TTLが切れたら切断されてしまう

そのため、今後はRemoteCommandで定期的にセッションを作成し、tmux,screen,autosshなどで自動再接続みたいにしたいなと思っています。

# おわりに

今回はセッションをcurlで作成しました。できれば二度とやりたくないと思うほど面倒でした。CLIを使わないと大変だということを思い知りました。
まだ課題はあるので、勉強します。