🦔

詳解GKE: Container-Optimized OSへのSSH接続

2020/12/05に公開

前回の記事

はじめに

GKEノードのデフォルトのOSはContainer-Optimized OS(以降COS)です。GKEの詳解に入る前にCOSのデバッグ方法について記述しようとしましたが、その前段のSSH接続だけで結構な分量になってしまったので、まずはSSH接続の仕組みについて解説をします。次回はまだデバッグ方法にたどり着かず、 compute-image-packages について解説をしていきます。なかなか本編に入れませんが、GKEを知るためには、GCEについての深堀りが必要ということです。

GKEノードへのSSH接続

限定公開クラスタの場合、GKEノードへのパブリックIPが付与されないため、インターネットから直接SSH接続することはできません。GCPにはユーザ認証をしつつトンネリングをすることができるIdentity-Aware Proxyという機能があります。

まずはアクセスしてみましょう。

IAP用ファイアウォールルールの追加

IAPは 35.235.240.0/20 のネットワークアドレスを利用しています。パット見はグローバルアドレスですが、パブリックIPを持たないGKEノードからでも通信することができます。同様の性質のアドレスとしてロードバランサのヘルスチェック用ネットワークアドレス 130.211.0.0/22,35.191.0.0/16 があります。[1] パブリックIPを持たないGKEノードからグローバルIPアドレスにアクセスできる「限定公開の Google アクセス」という仕組みがあります。[2]こちらは完全にグローバルIPアドレスにアクセスできるため、IAPのネットワークとは扱いが異なりそうです。

IAPを利用するために、35.235.240.0/20 からGKEノードへのSSH通信を許可するファイアウォールルールを追加します。

端末
% gcloud compute firewall-rules create allow-ssh-ingress-from-iap \
  --network=gke-network \
  --direction=INGRESS \
  --action=allow \
  --rules=tcp:22 \
  --source-ranges=35.235.240.0/20

SSH接続

準備ができたため、gcloudコマンドでGKEノードにアクセスしてみます。

端末
 % gcloud compute instances list
NAME                                    ZONE        MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP  STATUS
gke-cluster-default-pool-3e89aeb0-b47k  us-west1-a  e2-medium     true         10.128.0.4                RUNNING
gke-cluster-default-pool-3e89aeb0-xx3q  us-west1-a  e2-medium     true         10.128.0.3                RUNNING
gke-cluster-default-pool-41ef50b9-5zfg  us-west1-b  e2-medium     true         10.128.0.2                RUNNING

% gcloud beta compute ssh --zone "us-west1-a" "nat@gke-cluster-default-pool-3e89aeb0-b47k" --tunnel-through-iap --project "gke-detail"
Updating project ssh metadata...⠶Updated [https://www.googleapis.com/compute/beta/projects/gke-detail].                                                                                                  
Updating project ssh metadata...done.                                                                                                                                                                    
Waiting for SSH key to propagate.

Welcome to Kubernetes v1.17.12-gke.1504!

You can find documentation for Kubernetes at:
  http://docs.kubernetes.io/

The source for this release can be found at:
  /home/kubernetes/kubernetes-src.tar.gz
Or you can download it at:
  https://storage.googleapis.com/kubernetes-release-gke/release/v1.17.12-gke.1504/kubernetes-src.tar.gz

It is based on the Kubernetes source at:
  https://github.com/kubernetes/kubernetes/tree/v1.17.12-gke.1504

For Kubernetes copyright and licensing information, see:
  /home/kubernetes/LICENSES

nat@gke-cluster-default-pool-3e89aeb0-b47k ~ $

nat@gke-cluster-default-pool-3e89aeb0-b47k ~ $ id
uid=20142(nat) gid=20143(nat) groups=20143(nat),4(adm),27(video),412(docker),20141(google-sudoers)

あっさりログインすることができました。あえて nat というユーザ名を指定して gcloud コマンドを実行しています。これは特にGCP上で定義しているユーザではなく、今考えて指定したユーザ名です。AWSをよく利用される方であれば、ログイン時に指定したユーザでログインできていることに違和感を覚えると思います。AWSの標準的なインスタンスのデプロイ方法では、 cloud-user 等の単一ユーザが cloud-init によって作成され、事前作成したキーチェーンの公開鍵をmetadataから取得して、 authorized_keys に登録するだけです。

この仕様はGKEノードに限った話ではなく、GCEのの仕様です。GCEに任意のユーザ名でSSHログインがでる仕組みを説明していきます。

GCEのSSH認証方式

GCEへのSSH認証方式は大きく分けて2種類存在します。

メタデータ内のSSH認証鍵の管理

GCEのプロジェクトメタデータ、もしくはインスタンスメタデータに登録されているSSH公開鍵を利用してログインする方式です。[3] これについては後半で詳しく説明します。

OS Login

メタデータによる認証鍵には以下のデメリットがあります。

  • メタデータに登録されている公開鍵とユーザ名を手動で管理する必要がある
  • 特定のインスタンスのみにアクセス可能な権限を付与したい場合、インスタンスメタデータにSSH公開鍵を個別登録する必要がある
  • sudo権限が自動的に付与される

これらの理由からメタデータ内のSSH認証鍵の管理は現在では非推奨となり、代替としてOS Loginが推奨されています。[4] OS Loginを利用することで、LinuxユーザーとGoogle IDを直接関連付できるようになったり、IAMでsudo権限を付与することも可能になります。

しかし、悲しいことにGKEではOS Login機能を利用することができません。 [5]

制限事項
現在、Google Kubernetes Engine(GKE)で OS Login はサポートされていません。OS Login が有効になっている場合、GKE クラスタのノードは引き続きメタデータ SSH 認証鍵を使用します。

COSは基本的にOSの設定を変更することができないため、必然的にメタデータ内のSSH認証鍵の管理をすることになります。

メタデータ内のSSH認識鍵の管理の仕組み

メタデータによるSSH認証鍵の管理方式について掘り下げます。

Google Compute Engine Accounts Daemon

この仕組みを実現しているのは、Google Compute Engine Accounts Daemonと呼ばれるCOS上で動作しているデーモンです。

GKEノード
# systemctl status google-accounts-daemon
● google-accounts-daemon.service - Google Compute Engine Accounts Daemon
   Loaded: loaded (/lib/systemd/system/google-accounts-daemon.service; disabled; vendor preset: disabled)
  Drop-In: /lib/systemd/system/google-accounts-daemon.service.d
           └─00-system-sysdaemons-slice.conf
   Active: active (running) since Wed 2020-12-02 13:05:36 UTC; 1h 45min ago
 Main PID: 377 (google_accounts)
    Tasks: 1 (limit: 4684)
   Memory: 15.5M
      CPU: 2.717s
   CGroup: /system.slice/system-sysdaemons.slice/google-accounts-daemon.service
           └─377 /usr/bin/python2.7 /usr/lib/python-exec/python2.7/google_accounts_daemon

google-accounts-daemon.service というサービス名で登録されていますね。

このサービスのパッケージ google-compute-engine はCOSやGoogleが提供しているイメージなどにプリインストールされていています。自身で作ったイメージにインストールすることもできます。[6] google-compute-engine をインストールすると、google-accounts-daemon.service 以外にも複数のデーモンが起動します。これらのデーモンについては次回説明します。

ソースコード

google-accounts-daemon のソースコードは こちら にあります。

gcloudコマンド(Google Cloud SDK)のソースコードはGithubのようなリポジトリは見つけることができませんでしたが、こちらのページのリンク先アーカイブからダウンロードすることができるようです。[7]

% wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-319.0.0-linux-x86_64.tar.gz

処理概要

gcloud beta compute ssh コマンド実行時、以下のような処理が行われます。

  1. 端末上のにSSHキーペアが存在しない場合、 ~/.ssh/google_compute_engine~/.ssh/google_compute_engine.pub のキーペアが作成されます。これは初回のみ実行されます。

  2. 作成されたSSHキーペアの公開鍵をGCEのプロジェクトメタデータに登録します。
    登録されているプロジェクトメタデータは以下のコマンドで参照できます。

    端末
    % gcloud compute project-info describe            
    commonInstanceMetadata:
      fingerprint: 6JQRGHnMvRU=
      items:
      - key: gke-cluster-f0a2ee52-secondary-ranges
        value: services:gke-network:gke-subnet:gke-cluster-services,shareable-pods:gke-network:gke-subnet:gke-cluster-pods
      - key: ssh-keys
        value: |-
        nat:ssh-rsa XXX...XXX nat
      kind: compute#metadata
    creationTimestamp: '2020-12-01T04:13:48.696-08:00'
    ...() 
    
  3. google-accounts-daemonがメタデータの変更を検知し、公開鍵を取得。取得元はプロジェクトメタデータとインスタンスメタデータです。
    メタデータはインスタンス内からアクセス可能なリンクローカルアドレス 169.254.169.254 からHTTPで取得することができます。これはどのクラウドサービスでも同じですね。ノード内からcurlで取得してみましょう。

    GKEノード
    # プロジェクトメタデータ
    $ curl -H  Metadata-Flavor:Google  "http://169.254.169.254/computeMetadata/v1/project/attributes/ssh-keys"
    nat:ssh-rsa XXX...XXX nat
    
    # インスタンスメタデータ
    $ curl -H  Metadata-Flavor:Google  "http://169.254.169.254/computeMetadata/v1/instance/attributes/ssh-keys"
    alice:ssh-rsa YYY...YYY alice    
    

    取得できていますね。google-accounts-daemonが実際に取得しているエンドポイントはメタデータのルート http://169.254.169.254/computeMetadata/v1 です。クエリパラメータに recursive=true を設定してメタデータ全体のデータをポーリングしています。

  4. google-accounts-daemonが管理する最新のユーザ一覧と、取得したメタデータを比較し、不足しているユーザがいればグループとユーザの追加をします。この時、ssh-keysの一番左の項目をグループ名、ユーザ名として利用します。
    ユーザ一覧は /var/lib/google/google_users に格納されています。

    GKEノード
    # cat /var/lib/google/google_users 
    alice
    nat
    

    だいぶシンプルですね。このリストの差分を見て、ユーザの追加、削除の判定を行っています。

  5. 新規作成したユーザをadm、video、dockerグループに所属させます。所属させるグループの一覧は /etc/default/instance_configs.cfg に定義されています。
    一覧のグループが存在していれば所属させるという仕組みになっています。

    GKEノード
    # grep "^groups" /etc/default/instance_configs.cfg
    groups = adm,dip,docker,lxd,plugdev,video
    
  6. 新規作成したユーザをsudoersに登録します。
    sudoersに登録すると言っても、実態はgoogle-sudoersグループに所属させているだけです。sudoersの設定は以下のようになっています。

    GKEノード
    # cat /etc/sudoers.d/google_sudoers 
    %google-sudoers ALL=(ALL:ALL) NOPASSWD:ALL
    
  7. 作成したユーザのホームディレクトリの ~/.ssh/authorized_keys に公開鍵を保存します。

    GKEノード
    # cat /home/nat/.ssh/authorized_keys 
    # Added by Google
    nat:ssh-rsa XXX...XXX nat
    

    登録されていますね。

  8. SSH接続
    ここまでくれば、あとは公開鍵認証でSSH接続をするだけです。実際にはIAP経由で接続していますがIAP部分は省略しています。

①〜②の処理はgcloudコマンドが自動で行っていますが、gcloudコマンドを使わなくてもメタデータに公開鍵を手動で登録することで③〜⑦の処理が動作します。

試しにインスタンスメタデータにユーザ bob の公開鍵を登録してみましょう。

端末
% gcloud compute instances add-metadata gke-cluster-default-pool-3e89aeb0-l584 --metadata 'ssh-keys=bob:YYY...YYY bob'
Updated [https://www.googleapis.com/compute/v1/projects/gke-detail/zones/us-west1-a/instances/gke-cluster-default-pool-3e89aeb0-l584].
GKEノード
# journalctl -u google-accounts-daemon
()
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 google-accounts[379]: INFO Creating a new user account for bob.
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 useradd[48729]: new group: name=bob, GID=20145
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 useradd[48729]: new user: name=bob, UID=20144, GID=20145, home=/home/bob, shell=/bin/bash
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 google-accounts[379]: INFO Created user account bob.
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 usermod[48734]: add 'bob' to group 'adm'
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 usermod[48734]: add 'bob' to group 'video'
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 usermod[48734]: add 'bob' to group 'docker'
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 google-accounts[379]: INFO Adding user bob to the Google sudoers group.
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 google_accounts_daemon[379]: Adding user bob to group google-sudoers
Dec 04 09:32:11 gke-cluster-default-pool-3e89aeb0-l584 gpasswd[48739]: user bob added by root to group google-sudoers

# id bob
uid=20144(bob) gid=20145(bob) groups=20145(bob),4(adm),27(video),412(docker),20141(google-sudoers)

ユーザ追加処理が行われました。この状態で手動登録した公開鍵がauthorized_keys登録されるため、SSHログインができる状態になっています。

SSHのアクセス権限

処理内容からSSHログインできる条件は以下2つであることがわかりました。

  1. GKEノードのTCP:22に到達可能であること
    • 例1) IAPの権限が付与され、IAPからGKEノードへのVPC Firewallが許可されている
    • 例2) パブリックIPを持つGKEノードを作成し、端末からGKEノードへのVPC Firewallが許可されている
  2. GCEのプロジェクトメタデータ、もしくはインスタンスメタデータの更新権限があること
    • 例1) IAMで「Compute インスタンス管理者(v1)」権限が付与されている
    • 例2) IAMで「編集者」権限が付与されている

ここで重要なのは、SSHログインできるだけでなく、sudo権限も付与されていることです。メタデータ更新権限があるということは実質インスタンスの管理者であるため、sudo権限がついていても違和感は無いものの、AWSのキーペア管理とは全く思想が異なるため、 「想定外のメンバに必要以上の権限を与えていた」 ということがないよう注意しましょう。OS Loginが利用できるようになればこれらの問題は解消されるのですが、現時点でGKEで利用できません。

プロジェクト全体の公開 SSH 認証鍵の使用をブロックする

プロジェクトメタデータにSSH公開鍵を登録すると、全てのインスタンスにユーザが作成され、ログインできる状態になってしまいます。これをブロックしてインスタンスメタデータのみを利用する設定をすることができます。[8]
設定はブロックしたいインスタンスのメタデータに block-project-ssh-keys=TRUE を設定するのみです。このメタデータが設定されているインスタンスは、プロジェクト全体のメタデータに登録されているSSH公開鍵を無視するようになります。ノードプール作成時にメタデータを設定することができますが、ノードプール全体のメタデータを後から更新するオペレーションが用意されていません。インスタンス個別にメタデータを更新することはできますが、GKEノードはスケールアウト/インによって動的に作成されたり削除されたりするため、個別設定する運用は現実的ではありません。そのため、 「プロジェクト全体のSSH認証鍵を利用し、GKEノードへSSHログイン可能なアクタを分けたい要件が出てきた場合GCPプロジェクトを分ける」 という方針とするのがよいでしょう。

「GKEノードのTCP:22に到達可能であること」の罠

VPC FirewallでSSHポートを許可するルールを作成しなければ、メタデータの更新権限があったとしても到達性が無いため、SSHを利用できない状態にすることができるのでは、と思う方もいるかもしれません。これには注意が必要です。

Kubernetesのネットワークの実装要件として以下のものがあります。[9]

  • ノード上のPodが、NATなしですべてのノード上のすべてのPodと通信できること
  • systemdやkubeletなどノード上にあるエージェントが、そのノード上のすべてのPodと通信できること

Kubernetesのネットワークの実装はインフラや採用するCNIによって仕様が異なりますが、Network Policyを有効化したGKEの場合2つめの要件に引きずられているせいなのか、全てのPodから全てノードへの通信が可能となっています。

実際にアクセスしてみましょう。

端末
# sshクライアント用Podの作成
% cat << _EOF_ | kubectl apply -f -                                           
apiVersion: v1
kind: Pod
metadata:
  name: ssh-client
spec:
  containers:
    - name: ssh-client
      image: kroniak/ssh-client
      command: ["sleep", "3600"]
_EOF_

# google_compute_engineをPodに送信
% kubectl cp ~/.ssh/google_compute_engine ssh-client:google_compute_engine 

# Podに接続
% kubectl exec -it ssh-client -- /bin/bash
bash-5.0#
Pod
# GKEノードにssh接続
bash-5.0# ssh 10.128.0.9 -l nat -i google_compute_engine 

Welcome to Kubernetes v1.17.12-gke.1504!

(ログインメッセージ略)

nat@gke-cluster-default-pool-3e89aeb0-l584 ~ $ 

Pod経由でノードにSSH接続することができました。

OpenStackやVMWareを運用したことがある方からすると、ゲストであるPodからホストであるのGKEノードの管理ネットワークにアクセスできることに違和感を覚えると思います。OpenStackやVMWareでそのような設計をすることは稀です。GKEに関わらず、Kubernetesのネットワークの実装はKubernetesのネットワークの要件を受けて、デフォルトで全コンポーネントが相互に通信できる状態になってしまっていることが多いです。sshdだけでなく、kubeletやkube-systemの各種Podにもアクセス可能であるため、デフォルトの状態で本番運用をするには少なからずリスクが伴います。具体的にどのようなリスクがあるのか、どのような対策が必要かという観点で後日記事を書く予定です。

おまけ: sshコマンドでGCEにアクセスする

パブリックIPを持たないGCEにIAPを利用してSSH接続をする場合、gcloud beta compute sshを利用しますが、ansibleで管理したい場合など、sshコマンドで接続したい場面があると思います。sshコマンドでGCEにアクセスする設定を紹介します。

端末の ~/.ssh/config に以下設定を追加します。

~/.ssh/config(凡例)
Host gke-<GKEクラスタ名>-*
  User <SSH公開鍵の登録ユーザ名>
  IdentityFile ~/.ssh/google_compute_engine
  ProxyCommand sh -c 'gcloud compute start-iap-tunnel %h %p --listen-on-stdin --project <GKEのプロジェクト名> --zone "`gcloud compute instances list --filter %h --format \"value(zone)\"`"'

HostにはGCEインスタンス名を指定します。GKEの場合、ノード名はクラスタ名から始まるため、この設定は全てのGKEノードに適用されます。Userは公開鍵登録時に設定したユーザ名。GKEのプロジェクト名は固定で設定してください。

私の場合以下のような設定になります。

~/.ssh/config
Host gke-cluster-*
  User nat
  IdentityFile ~/.ssh/google_compute_engine
  ProxyCommand sh -c 'gcloud compute start-iap-tunnel %h %p --listen-on-stdin --project gke-detail --zone "`gcloud compute instances list --filter %h --format \"value(zone)\"`"'

設定はこれだけです。GKEノード名でssh接続できることを確認します。

端末
% gcloud auth login

% gcloud compute instances list 
NAME                                    ZONE        MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP  STATUS
gke-cluster-default-pool-3e89aeb0-l584  us-west1-a  e2-medium     true         10.128.0.9                RUNNING
gke-cluster-default-pool-41ef50b9-5lj5  us-west1-b  e2-medium     true         10.128.0.12               RUNNING

# gke-cluster-default-pool-3e89aeb0-l584 にログイン
% ssh gke-cluster-default-pool-3e89aeb0-l584
Welcome to Kubernetes v1.17.12-gke.1504!

(ログインメッセージ略)

nat@gke-cluster-default-pool-3e89aeb0-l584 ~ $ 

ログインできました。

まとめ

  • 限定公開クラスタのGKEノードにIAPでSSH接続することが可能
  • GCEのSSH認証方式には「メタデータ内のSSH認証鍵の管理」と「OS Login」の2種類があるが、GKEで「OS Login」を利用することはできない
  • GKEノード上のgoogle-accounts-daemonがメタデータに登録されているSSH公開鍵をインプットにユーザ作成、authorized_keysの更新を行っている
  • PodからGKEノードのsshdへの到達性があることに注意が必要

次回は google-compute-engine パッケージについて記事を書く予定です。

脚注
  1. ヘルスチェックの作成 ↩︎

  2. 限定公開の Google アクセスの構成 ↩︎

  3. メタデータ内の SSH 認証鍵の管理 ↩︎

  4. OS ログイン ↩︎

  5. OS ログインの制限事項 ↩︎

  6. ゲスト環境をインプレースでインストールする ↩︎

  7. バージョニングされたアーカイブからのインストール ↩︎

  8. Linux インスタンスによるプロジェクト全体の公開 SSH 認証鍵の使用を許可またはブロックする ↩︎

  9. Kubernetesのネットワークモデル ↩︎

Discussion