TL; DR;
宅鯖のVMへの認証情報をリポジトリ内に置かなくて済むようにパスワードレス・Hashicorp Vault SSH OTPによる接続ができるようにしました。 その際、cloud-init・terraformを用いた自動セットアップや、AnsibleでのOTPを使ったログインなど、IaC awareな方法を用いました
モチベーション
我が家の宅鯖環境では、環境の再現性を高めるために、物理マシン上にKVMによる仮想マシンを構築して、その上でKubernetesクラスタを構築しています。 このVMへの接続に用いるユーザーは、VMインストール時にcloud init(正確にはubuntuのauto install)を用いてパスワード作成・SSHアクセスを許可する公開鍵登録を行っているのですが、このやり方だと以下のような問題があります。
- 認証情報を平文で保存しておりセキュリティ的によろしくない
- プライベートリポジトリなのでそこまで問題ではないが気持ち悪い
- 認証情報をローテーションする際に、cloud initの設定ファイル変更に加えてマシンでの変更が必要になる
- cloud initはインストール時にしか作用しないため
- マシン毎に異なる認証情報を用いようとするとさらに管理が煩雑になる
これらの問題に対して、本来ならPAMを使って外部のパスワードソースと連携するといったアプローチが考えられますが、インターネットからアクセスできないネットワーク上かつユーザーは自分だけなので、とりあえずパスワードレスとします。
ただしそのままだとSSH接続をセキュアに行うことができないので、SSH接続時の認証に関して以下のような方法を検討し、最終的にHashicorp VaultのSSH OTPを導入することにしました。 機能だけで見るならTeleportですが宅鯖環境での設計は一筋縄ではいかなさそうである一方、SSH OTPなら機密情報管理用にVaultを既に入れていてミニマルに始められそうだったためです。
- 公開鍵認証
- メリット
- 公開鍵を配置するだけで実装可能
- デメリット
- 自動化の余地はあるが、接続元の追加・削除の際に全マシンに対して変更する必要がある
- メリット
- Hashicorp Vault SSH OTP
- Vaultが生成したOTPを使うことでそのマシンへのログインに限ったSSHパスワードを行える
- メリット
- エージェントをインストールするだけでマシンへの導入が終わる
- デメリット
- Hashicorp Vaultをセットアップする必要がある
- Vaultへの疎通がとれない場合にログインできなくなる
- Teleport
- ゼロトラストのアクセス用OSS
- メリット
- PAMの提供もしており、セキュリティレベルでは最も高い
- デメリット
- 導入・運用の手間が多い
- 宅鯖でHA構成を取ろうとするとストレージ・LBの選定が難しい
Hashicorp Vault SSH OTPによるログインの流れは図のようになります。
設定
Vault側の設定
公式ドキュメントの手順に従ってTerraformによる設定を行います。
provider "vault" {
address = "Vaultのエンドポイント"
ca_cert_file = "ルート証明書"
}
// ssh secret engineを有効化する
resource "vault_mount" "ssh" {
path = "ssh"
type = "ssh"
}
// roleを作成する
resource "vault_ssh_secret_backend_role" "virtual-machine-otp" {
name = "virtual-machine-otp"
backend = vault_mount.ssh.path
key_type = "otp"
default_user = "localadmin"
cidr_list = "172.16.1.0/24"
}
仮想マシン側の設定
公式チュートリアルに従ってcloud-initでのVMセットアップ時に設定を行います。 実際にはubuntuのautoinstallという機能の設定ファイルなので、user-data以下にcloud-initの記法で記述します。
#cloud-config
autoinstall:
version: 1
locale: ja_JP.UTF-8
user-data:
users:
- name: localadmin
# パスワードを指定しないことでパスワードレスにできる
# パスワードレスでsudoができるように設定
sudo: "ALL=(ALL) NOPASSWD:ALL"
shell: /bin/bash
runcmd:
- apt update
- apt install -y unzip
# vault-ssh-helper(エージェント)のインストール
- wget "https://releases.hashicorp.com/vault-ssh-helper/0.2.1/vault-ssh-helper_0.2.1_linux_amd64.zip"
- unzip -q vault-ssh-helper_0.2.1_linux_amd64.zip -d /usr/local/bin
- rm vault-ssh-helper_0.2.1_linux_amd64.zip
- chmod 0755 /usr/local/bin/vault-ssh-helper
- chown "root:root" /usr/local/bin/vault-ssh-helper
- mkdir /etc/vault-ssh-helper.d
# vault-ssh-helperの設定
- |
tee /etc/vault-ssh-helper.d/ca.crt << EOF
ルート証明書の内容を記載
EOF
- |
tee /etc/vault-ssh-helper.d/config.hcl <<EOF
vault_addr = "Vaultのエンドポイント"
tls_skip_verify = false
ca_cert = "/etc/vault-ssh-helper.d/ca.crt"
ssh_mount_point = "ssh"
allowed_roles = "*"
EOF
# sshdのPAM設定でvault-ssh-helperを使うように変更する
- sed -r -i "s/(^@include common-auth)/#\1/" /etc/pam.d/sshd
- |
tee -a /etc/pam.d/sshd <<EOF
auth requisite pam_exec.so quiet expose_authtok log=/var/log/vault-ssh.log /usr/local/bin/vault-ssh-helper -config=/etc/vault-ssh-helper.d/config.hcl
auth optional pam_unix.so not_set_pass use_first_pass nodelay
EOF
# sshdの設定でvault-ssh-helperを使うように変更する
- sed -r -i "s/(^UsePAM no)/#\1/" /etc/ssh/sshd_config
- sed -r -i "s/(^KbdInteractiveAuthentication no)/#\1/" /etc/ssh/sshd_config
- sed -r -i "s/(^PasswordAuthentication yes)/#\1/" /etc/ssh/sshd_config
- |
tee -a /etc/ssh/sshd_config <<EOF
# For Hashicorp vault ssh helper
KbdInteractiveAuthentication yes
UsePAM yes
PasswordAuthentication no
EOF
- systemctl restart sshd
- vault-ssh-helper -verify-only -config /etc/vault-ssh-helper.d/config.hcl
timezone: "Asia/Tokyo"
ssh:
install-server: true
この設定を投入してVMを作成すると、自動でSSH OTPのエージェントのインストール・設定が完了します。
ログイン
ターミナルからのログイン
VaultのUIから、OTPを発行し、そのOTPをSSH時に求められるパスワードとすることでログインできます。
パスワードレスの設定をしているので、sudoをする際にはパスワードを求められることはありません。
また、いちいちUIにアクセスするのは面倒ですが、自動で行えるコマンドも紹介されています。
Ansibleによるログイン
IaCを実践するために、kubernetesノードとしてのセットアップなどはAnsibleで行っていますが、OTPなのでinventoryに直接書くことができません。
そこで、inventoryでOTPを動的に作成してそれを認証情報として使えるようにします。
仕組みとしては簡単で、community.hashi_vault.vault_write lookupプラグインを用いて、OTPを取得しているというだけです。
all:
vars:
ansible_user: localadmin
ansible_password: "{{ lookup('community.hashi_vault.vault_write', 'ssh/creds/virtual-machine-otp', url='Vaultのエンドポイント', ca_cert='ルート証明書のパス', data={'ip': lookup('community.general.dig', inventory_hostname)})['data']['key'] }}"
children:
servers:
hosts:
VM1のFQDN:
VM2のFQDN:
...
今後の改善点
当初実現したかったことは達成でき当面はこれで行こうかと思いますが、パスワードレスにしたゆえにセキュリティ的に少し気持ち悪いという課題は残ります。
Teleportはここらへんをうまく解決してくれそうなので、検証をしていきたいです。