🌉

~/.ssh/configファイルをGitリポジトリで管理する

2022/12/08に公開1

リモートホストにSSH接続する際に便利な ~/.ssh/configファイルを、Git管理し、かつSSH踏み台ホスト上へのデプロイも簡単にできるようにする、というプロジェクトの実装例です。

実装自体は、以下のGitHubリポジトリに公開しています。複雑なことはやっていないので、多分リポジトリのコードを直接みるのが一番早いです。

この記事では、実装の考え方を中心に、このリポジトリの出来上がり方を記載しています。

~/.ssh/configファイルとは

~/.ssh/configファイルとは、sshコマンドでいつも指定する接続情報をあらかじめ記載(複数可)しておき、ssh <接続先のエイリアス>でスッとSSH接続できるようにする、ありがたいファイルです。

~/.ssh/configの例(各Hostは適当です)
# Development Host (Amazon Linux 2)
Host devel
  HostName 192.168.1.10
  User ec2-user
  IdentityFile ~/.ssh/devel.pem

# Mail Host (Ubuntu)
Host mail
  HostName 192.168.2.50
  User ubuntu
  IdentityFile ~/.ssh/mail.pem
コマンド例
$ ssh devel # これと同じ:ssh -i ~/.ssh/devel.pem ec2-user@192.168.1.10

詳細は割愛します。参考に上げたリンクを確認するか、「ssh config」でググってみてください。

AWS EC2インスタンスの踏み台サーバ利用について

AWS Systems Manager (SSM) Session Managerを使うことで、SSM AgentをインストールしたEC2インスタンスに対しては、SSH踏み台ホスト無しで直接アクセスするようにできます[1]。検討してみてください。

ただし、SSM Agentをインストールできない製品VMでは利用できなかったり、いくつかの要件が合わなかったり[2]で、やむを得ずSSH踏み台ホストを作成・利用するケースもあると思います。

前提

「踏み台ホストから他のホストにSSH接続するための秘密鍵(例:*.pem)は、~/.ssh/配下のどこかに置いてある」という想定とします。つまり、独自の専用のサブディレクトリ(深さ問わず)に秘密鍵を集めていても大丈夫です。

OKなディレクトリ構成の例
OKなディレクトリ構成の例

実装方針

まずは、実装の方針を確認しましょう。

  1. ~/.ssh/配下のファイルのGit管理について:

    • する:config
    • しない:SSHのキー(authorized_keys, 秘密鍵)、known_hosts, known_hosts.old
  2. ~/.ssh/configの調整・更新作業について:

    • ~/.ssh/ディレクトリにて、直接作業ができるようにする
    • ごく普通の、configファイル編集、git add configgit commitgit pushという単純なフローになるようにする
  3. 簡単なデプロイ操作にする

    1. 初期デプロイ:
      『最新の~/.ssh/configに置き換え』+『~/.ssh/ディレクトリのGitリポジトリ化』
    2. 2回目以降のデプロイ:
      『最新の~/.ssh/configに置き換え』

デプロイ操作イメージ

3の「簡単なデプロイ操作」について、具体的に考えておきます。

3-1. 初期デプロイ

初期デプロイでは、『最新の~/.ssh/configに置き換え』+『~/.ssh/ディレクトリのGitリポジトリ化』を同時に行います。

手数少なく、かつ適当なディレクトリで作業が完遂できるように実装することにします。

適当なディレクトリにて(ディレクトリ名ssh-bastion-configとしてgit clone)
$ git clone <リモートGitリポジトリのURL> ssh-bastion-config

$ ./ssh-bastion-config/deploy.sh # デプロイ用シェルスクリプトの実行
# =>
#    * 最新の~/.ssh/configに置き換え
#    * ~/.ssh/ディレクトリのGitリポジトリ化
#    * git cloneしてきた./ssh-bastion-config/ディレクトリを削除(掃除)

ここで、deploy.shに期待する動作要件は、次のとおりです:

  • git cloneしてきたディレクトリ内のconfigファイルを~/.ssh/配下に配置する
  • ~/.ssh/をGitリポジトリにする
  • 今適当な場所にgit cloneしてきたディレクトリを、削除する(掃除)

3-2. 2回目以降のデプロイ

2回目以降のデプロイでは、『最新の~/.ssh/configに置き換え』を行います。

git pullを活用します。

$ cd ~/.ssh/

(~/.ssh)$ git pull
# => 最新の~/.ssh/configに置き換え、他のファイルはそのまま

ここで、git pullに期待する動作要件は、次のとおりです:

  • ~/.ssh/configファイルに更新が走る
  • ~/.ssh/配下の、既存のSSHのキー(authorized_keys, 秘密鍵)、known_hosts, known_hosts.oldはそのまま
  • (Gitリポジトリ用のファイルももちろん更新が走る(README.md, .gitignoreなど))

git pullによる更新ファイルイメージ(SSH関係のファイルのみ記載)
git pullによる更新ファイルイメージ(SSH関係のファイルのみ記載)

実装

configファイル

まず、conifgファイルを作成します。

config(適当なサンプルレベルです)
# Development Host (Amazon Linux 2)
Host devel
  HostName 192.168.1.1
  User ec2-user
  IdentityFile ~/.ssh/devel.pem

あるいは、次のように、踏み台サーバにてconfigファイルを直接作成してもよいです(こちらの方が調整も容易です):

  1. 一旦全部コメントアウト(#を行の先頭に置く)した雛形configファイルを作っておく
  2. このあと作る.gitignoreファイルとdeploy.shと一緒にコミットして、リモートGitリポジトリにgit push
  3. 踏み台ホスト上で、リモートGitリポジトリを初期デプロイ(git cloneしてdeploy.sh実行)
  4. ~/.ssh/ディレクトリがGitリポジトリになっているので、踏み台ホスト上で~/.ssh/configを調整しながら作成する(フロー詳細は、次節[使い方補足] configファイルの更新フローを参照)

Git管理するファイルを決める(.gitignore)

次に、.gitignoreファイルを、以下のように作成します:

.gitignore
# Ignore SSH keys.
authorized_keys
*.pem

# Ignore known_hosts, known_hosts.old
known_hosts*

こうすれば、SSHに関係するファイルについて、以下をすべて満たすことができます:

  • ~/.ssh/配下のファイルのGit管理について(実装方針1):

    • する:config
    • しない:SSHのキー(authorized_keys, 秘密鍵)、known_hosts, known_hosts.old
  • ~/.ssh/configの調整・更新作業について(実装方針2):

    • ~/.ssh/ディレクトリにて、直接作業ができるようにする
    • ごく普通の、configファイル編集、git add config&git commit、git pushという単純なフローになるようにする
  • git pullしたときに...(実装方針3-2):

    • ~/.ssh/configファイルを更新
    • ~/.ssh/配下の、SSHのキー(authorized_keys, 秘密鍵)、known_hosts, known_hosts.oldはそのまま

初期デプロイ用スクリプト(deploy.sh)

最後に、実装方針3-1. 初期デプロイで使うスクリプトdeploy.shを作成します。

操作イメージと動作要件を再掲します:

適当なディレクトリにて(ディレクトリ名ssh-bastion-configとしてgit clone)
$ git clone <リモートGitリポジトリのURL> ssh-bastion-config

$ ./ssh-bastion-config/deploy.sh # デプロイ用シェルスクリプトの実行
  • git cloneしてきたディレクトリ内のconfigファイルを~/.ssh/配下に配置する
  • ~/.ssh/をGitリポジトリにする
  • 今適当な場所にgit cloneしてきたディレクトリを、削除する(掃除)

実際には、次のような動作で、これらの要件を満たすようにします:

  1. ~/.ssh/配下の、SSHのキー(authorized_keys, 秘密鍵)、known_hosts, known_hosts.oldを、git cloneしてきたディレクトリに持ってくる
    • 秘密鍵(例:*.pem)は、~/.ssh/配下のサブディレクトリに格納している可能性もある。その場合は同名のサブディレクトリを作成して、その中に秘密鍵を入れる
  2. git cloneしてきたディレクトリで、~/.ssh/ディレクトリを、上書きする
    • .git/ごと移動するので、Gitリポジトリになる
    • git cloneしてきたGitリポジトリに入っていたconfigファイルが~/.ssh/configになる
    • ついでにgit cloneしてきたディレクトリの削除(掃除)も完了する

このとき、元々の~/.ssh/ディレクトリに入っている「1で持ってこないファイル」については、2にて上書きされて消えてしまいます...というのは乱暴なので、そういうファイルがある場合はエラーで中断するようにします。


動作イメージ(秘密鍵用のサブディレクトリがあるパターン)

deploy.sh
#!/bin/bash

# 秘密鍵の拡張子('.'抜き)
private_key_ext="pem"

# ------------------------------------------------------------

# Gitリポジトリの中(=このdeploy.shと同じディレクトリ)に移動
cd $(dirname $0)

ssh_dirpath="${HOME}/.ssh"
repository_dirpath=$(pwd)

echo "Start to deploy ${ssh_dirpath}/config."

# 0. 余計なファイルが~/.sshディレクトリ配下に含まれていないかどうかの確認
disallowed_files=$(find ${ssh_dirpath} -type f \
                        \( ! -name authorized_keys \) \
                        -and \( ! -name "*.${private_key_ext}" \) \
                        -and \( ! -name "known_hosts*" \))

## 余計なファイルが含まれている場合は中止
if [ "${disallowed_files}" != "" ]; then
  echo -e "[\e[31mERROR\e[m] Please remove these files from ${ssh_dirpath}:"
  echo ${disallowed_files} | tr " " "\n" \
    | xargs -I @ echo "  x @"

  echo "Deployment canceled."
  exit 1
fi

# 1. 既存のSSHのキー(authorized_keysファイルと秘密鍵)、
#    known_hosts, known_hosts.oldを、このGitリポジトリのディレクトリに移動
echo "[1] Moved existing files:"

## authorized_keys
mv ${ssh_dirpath}/authorized_keys ./

echo "  * ${ssh_dirpath}/authorized_keys -> ${repository_dirpath}/authorized_keys"

## 秘密鍵
find ${ssh_dirpath} -type f -name \*.${private_key_ext} \
  | while read fullpath; do

    sed_script="s|^$(echo ${ssh_dirpath} | sed -r 's|\.|\\.|g')/?(.*)?$|\1|"
    subdirpath=$(dirname $(echo ${fullpath}) | sed -r "${sed_script}")

    # 秘密鍵がサブディレクトリにある場合は、サブディレクトリを作成
    if [ "${subdirpath}" != "" ] && [ ! -d ./${subdirpath} ]; then
      mkdir -p ./${subdirpath}
    fi

    mv ${fullpath} ./${subdirpath}

    filename=$(basename ${fullpath})
    echo "  * ${fullpath} -> ${repository_dirpath}/${subdirpath}/${filename}" | tr -s /
  done

## known_hosts*
find ${ssh_dirpath} -type f -name 'known_hosts*' \
  | while read fullpath; do
    mv ${fullpath} ./

    filename=$(basename ${fullpath})
    echo "  * ${fullpath} -> ${repository_dirpath}/${filename}"
  done

# 2. ~/.sshディレクトリを削除
rm -rf ${ssh_dirpath}

echo "[2] Removed ${ssh_dirpath}/ directory."

# 3. このGitリポジトリを~/.sshディレクトリに
mv ${repository_dirpath} ${ssh_dirpath}

echo "[3] Moved ${repository_dirpath}/ as a new ${ssh_dirpath}/ directory."

echo "Mission Accomplished!"
実行例
  • 無難に完了:
$ cd /tmp

(/tmp)$ ./ssh-bastion-host-config/deploy.sh
Start to deploy /home/ubuntu/.ssh/config.
[1] Moved existing files:
  * /home/ubuntu/.ssh/authorized_keys -> /tmp/ssh-bastion-host-config/authorized_keys
  * /home/ubuntu/.ssh/private_keys/baz.pem -> /tmp/ssh-bastion-host-config/private_keys/baz.pem
  * /home/ubuntu/.ssh/private_keys/devel/foo.pem -> /tmp/ssh-bastion-host-config/private_keys/devel/foo.pem
  * /home/ubuntu/.ssh/private_keys/devel/bar.pem -> /tmp/ssh-bastion-host-config/private_keys/devel/bar.pem
  * /home/ubuntu/.ssh/known_hosts -> /tmp/ssh-bastion-host-config/known_hosts
[2] Removed /home/ubuntu/.ssh/ directory.
[3] Moved /tmp/ssh-bastion-host-config/ as a new /home/ubuntu/.ssh/ directory.
Mission Accomplished!
  • 余計なファイルが含まれているのでエラーで終了(実際のメッセージのERRORは赤字です):
$ ./ssh-bastion-host-config/deploy.sh
Start to deploy /home/ubuntu/.ssh/config.
[ERROR] Please remove these files from /home/ubuntu/.ssh:
  x /home/ubuntu/.ssh/private_keys/baz
  x /home/ubuntu/.ssh/.foobar
  x /home/ubuntu/.ssh/config
Deployment canceled.

Step 2について、秘密鍵やknown_hosts, known_hosts.oldファイルは、初期デプロイ実施時点の踏み台ホスト~/.ssh/配下には存在しない可能性もあるので、findコマンドの結果で引っかかった分だけファイルの移動を実行しています。

また、工夫として、スクリプト実行時に環境変数HOMEを変更することで、別ユーザの~/ssh/configのセットアップが可能な形にしています([使い方補足] rootユーザとしてデプロイを行う場合の節を参照)。

そうそう、スクリプトファイル作成後に、実行権限を割り振るのを忘れないでください:

$ chmod 755 deploy.sh

これで実装は終わりです。
コミットして、リモートGitリポジトリにgit pushし、踏み台ホストにて使用します。

[使い方補足] configファイルの更新フロー

初回のdeploy.shによる踏み台ホスト上へのデプロイが済んでしまえば、以降は単純なフローになります:

  1. ~/.ssh/ディレクトリに移動する
  2. ~/.ssh/configファイルを直接編集する
  3. ~/.ssh/configファイルをgit addgit commitする
  4. リモートリポジトリにgit pushする

[使い方補足] rootユーザとしてデプロイを行う場合

ユーザ本人でなくrootユーザが代理でデプロイを行う場合、以下のようにデプロイ操作を変更してください(ユーザ「ec2-user」のセットアップ例です):

適当なディレクトリにて(ディレクトリ名ssh-bastion-configとしてgit clone)
$ git clone <GitリポジトリのURL> ssh-bastion-config # ここは同じ

$ HOME=/home/ec2-user ./ssh-bastion-config/deploy.sh # 変更点1

$ chown -R ec2-user:ec2-user /home/ec2-user/.ssh # 変更点2
  1. deploy.shは、環境変数HOMEの値で作用する.sshディレクトリが変わる。
    rootユーザではHOME=/root/なので、そのままdeploy.shを実行すると、/root/.sshが対象となってしまう。そこで、実行時のみHOMEを変更する
  2. ユーザ本人が.ssh/configを使えるように、ファイルの所有者・グループを変更する

SSHのキー(authorized_keys, 秘密鍵)とknown_hosts, known_hosts.oldの管理・デプロイについて

セキュリティの観点から、特にSSHのキー(authorized_keys, 秘密鍵)の管理・デプロイは、別のセキュアな方法で行ってください。

known_hostsやknown_hosts.oldファイルは、無くなっても問題はないです。
(これらファイルが無い状態で、踏み台サーバから別のホストにSSH接続すると、その初回に『接続しようとしているのはこのホストで問題ないですか』という認証が発生するだけ。)

参考

脚注
  1. ↩︎
  2. SSM Session Managerを使った踏み台サーバ構築(NRI Netcom BLOG):「SSM Session Managerを採用しなかった」記事です。どの要件を重視して採用するか否かを決める過程が書かれていて、参考になります。 ↩︎

Discussion

shuichishuichi

デプロイ用スクリプト、こんなややこしい動作にせずに、
「リポジトリ内の.git/, .gitignore, config, deploy.shを、~/.ssh/配下にmvする」
でよいのでは...?