🗂

Netflix Blessでサーバーレスな証明書ベースのSSH認証基盤を構築してみた

2022/03/31に公開約15,600字

OpenSSH の認証方式には、パスワード・公開鍵以外にも、証明書ベースの認証が可能です。

If You're Not Using SSH Certificates You're Doing SSH Wrong | Smallstep Blog

クライアントには認証局(CA)が署名した期限付きの証明書が発行され、サーバーは証明書が信頼する CA に署名されているかチェックするだけです。

システム管理者はユーザーの公開鍵の管理から開放され、有効期限が短いエフェメラルな証明書を発行すれば、証明書の漏洩リスクも軽減できます。

OpenSSH にのみ依存するため、オンプレ・クラウドやOSを透過にSSH接続できます。

本ブログでは、証明書ベースのユーザー認証方法と、Netflix が開発したサーバーレスな証明書発行基盤のBLESSについて解説します。

公開鍵認証の問題点

サーバーに SSH 接続する際には公開鍵認証がよく利用されますが、管理サーバーが多い場合、運用負荷が高い傾向があります。

新規に公開鍵を登録する場合や、利用者の移動・退職に伴い公開鍵を無効化する場合には、SSH 先の各サーバーで作業する必要があります。鍵が漏洩した場合はまさに悲劇です。

鍵の管理はスケールしにくいことから、アカウント・鍵を一元管理すべくLDAP・Kerberosが導入されることもありますが、これらを正しく運用するのはそう簡単では有りません。

証明書認証の利点

これらの課題を解決すべく、OpenSSH は 2010年3月リリースの OpenSSH 5.4 から PKI のような証明書ベースの認証にも対応しました。

ユーザー認証とホスト認証

証明書ベースの認証はユーザー認証ホスト認証の2種類があり、ユーザー認証はサーバーが接続してくるユーザーを認証し、ホスト認証はユーザーが接続先のホストを認証します。

今回はユーザー認証に限定して解説します。

ユーザー認証をやってみた

  • 認証局(CA)
  • ユーザー
  • ホスト

の3台のサーバーを用意し、実際にユーザー認証をやってみます。

1.CA : 公開鍵ペアを作成

CA 用の公開鍵のペアを作成します。

ユーザーの公開鍵をCA秘密鍵で署名して証明書を返し、ホストでは、証明書をCA公開鍵で検証します。

パスフレーズ付きで、公開鍵ペアを作成します。

$ mkdir ~/ca
$ cd ~/ca
$ ssh-keygen -t rsa -b 4096 -m PEM -f bless-ca- -C "SSH CA Key"
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): # パスフレーズを設定
Enter same passphrase again:
Your identification has been saved in bless-ca.
Your public key has been saved in bless-ca.pub.
The key fingerprint is:
...
$ ls -1
bless-ca-
bless-ca-.pub

2. ホストに CA の公開鍵を展開

証明書の検証のために、ホストサーバーにCAの公開鍵(bless-ca.pub)を展開します。

/etc/ssh/sshd_configTrustedUserCAKeys でこの公開鍵のパスを指定します。

$ sudo cp bless-ca-.pub /etc/ssh/
$ sudo tail /etc/ssh/sshd_config
#	X11Forwarding no
#	AllowTcpForwarding no
#	PermitTTY no
#	ForceCommand cvs server

AuthorizedKeysCommand /opt/aws/bin/eic_run_authorized_keys %u %f
AuthorizedKeysCommandUser ec2-instance-connect

# Bless用設定を追加
TrustedUserCAKeys /etc/ssh/bless-ca-.pub

sshd デーモンを再起動します。

$ sudo systemctl restart sshd

3. ユーザー:公開鍵ペアを作成

公開鍵ペアを作成します。
CA向けの鍵ペアと異なり、パスフレーズは不要です。

$ ssh-keygen -m PEM -f bless-client
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
...
$ ls -1
bless-client
bless-client.pub

作成された公開鍵(bless-client.pub)を CA サーバーに転送します。

4. CA:クライアントの公開鍵から証明書を作成

ユーザーの公開鍵(bless-client.pub)をCAの秘密鍵(bless-ca-)で署名して、証明書を作成します。

$ ls -1
bless-ca-           # CA秘密鍵
bless-ca-.pub
bless-client.pub    # User公開鍵

$ ssh-keygen \
  -s bless-ca- \    # CA 秘密鍵
  -I test-certificate \
  -n ec2-user \     # ログインユーザー名
  -V +1h \          # 証明書の有効期限
  bless-client.pub  # User公開鍵
Signed user key bless-client-cert.pub: id "test-certificate" serial 0 for ec2-user valid from 2022-03-29T13:08:00 to 2022-03-29T14:09:28

$ ls -1
bless-ca
bless-ca.pub
bless-client-cert.pub # 証明書
bless-client.pub

証明書を確認します。

$ ssh-keygen -L -f bless-client-cert.pub
bless-client-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com user certificate
        Public key: RSA-CERT SHA256:2llTVgZhClZ2UOxdN5ts9AcMYK9FgAuq3E6+Idv3ndg
        Signing CA: RSA SHA256:NQblA7U2K61fl3bTt7Y2NHYSHy3XwaDH+xD7EhxqCh0
        Key ID: "test-certificate"
        Serial: 0
        Valid: from 2022-03-29T13:08:00 to 2022-03-29T14:09:28
        Principals:
                ec2-user
        Critical Options: (none)
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

5. ユーザー:HOSTに接続

証明書(bless-client-cert.pub)を Client サーバーにコピーし、ホストサーバーに SSH します。

$ ls -1
bless-client
bless-client-cert.pub # 証明書
bless-client.pub

$ ssh -v -i bless-client ec2-user@172.31.13.229
...
debug1: Offering RSA-CERT public key: bless-client
debug1: Server accepts key: pkalg ssh-rsa-cert-v01@openssh.com blen 1612
debug1: Authentication succeeded (publickey).
...

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@server ~]$

ログから、証明書ベースの認証が成功したことがわかります。

Netflix Bless でユーザー認証をより便利に

上記手順では、証明書の発行手順が煩雑です。

このステップをAWS Lambdaでマネージド・サービス化したのが Netfix が開発した BLESS(Bastion’s Lambda Ephemeral Ssh Service)です。

AWS 上で Lambda を中心にサーバーレスに構築され、BLESS に公開鍵を渡すと、CA が署名した証明書を取得できます。

実行イメージ

  • ログインユーザー名
  • 接続元IPアドレス
  • 公開鍵

を JSON ファイルで用意します。

{
  "bastion_user": "ec2-user",
  "remote_usernames": "ec2-user",
  "bastion_user_ip": "172.31.17.236",
  "bastion_ips": "172.31.17.236",
  "command" : "",
  "public_key_to_sign": "ssh-rsa AAAAB... ec2-user@bless-client",
  "kmsauth_token": ""
}

この設定ファイル(特に公開鍵)を Lambda に渡すと、証明書を受け取れます。

$ aws lambda invoke \
  --invocation-type RequestResponse \
  --function-name bless \
  --payload fileb://input.json response.json
{
    "ExecutedVersion": "$LATEST",
    "StatusCode": 200
}

# レスポンスを確認
$ cat response.json
{
  "certificate": "ssh-rsa-cert-v01@openssh.com AAAAB...+GYZX3vLY3ulyA== ec2-user@bless-client"
}

# レスポンスから証明書を抜き出す
$ cat response.json | jq -r .certificate > bless-client-cert.pub

# 証明書を確認
$ ssh-keygen -L -f bless-client-cert.pub
bless-client-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com user certificate
        Public key: RSA-CERT SHA256:RfvG/05VCKCC1946T0CrPIcJeKxpg0sL8B4kjbr7lxs
        Signing CA: RSA SHA256:NQblA7U2K61fl3bTt7Y2NHYSHy3XwaDH+xD7EhxqCh0
        Key ID: "request[eb6a3d8f-af1d-4652-89e7-a381f02351e0] for[ec2-user] from[172.31.34.109] command[] ssh_key[RSA ea:00:8f:ca:6d:30:e7:83:74:07:59:38:2e:2f:ad:57]  ca[arn:aws:lambda:eu-central-1:205974338614:function:bless] valid_to[2022/03/29 13:33:20]"
        Serial: 0
        Valid: from 2022-03-29T13:29:20 to 2022-03-29T13:33:20
        Principals:
                ec2-user
        Critical Options:
                source-address 172.31.34.109
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc


# ホストに接続
$ ssh -i bless-client ec2-user@172.31.13.229

許可されていない IP アドレスからアクセスすると、エラーが発生します。

Authentication tried for ec2-user with valid certificate but not from a permitted host (ip=172.31.34.109).

証明書の有効期限(デフォルトは4分)が過ぎていると、エラーが発生します。

Mar 29 13:48:58 ip-172-31-46-73 sshd[5216]: error: AuthorizedKeysCommand /opt/aws/bin/eic_run_authorized_keys ec2-user SHA256:RfvG/05VCKCC1946T0CrPIcJeKxpg0sL8B4kjbr7lxs failed, status 22
Mar 29 13:48:58 ip-172-31-46-73 sshd[5216]: error: key_cert_check_authority: invalid certificate
Mar 29 13:48:58 ip-172-31-46-73 sshd[5216]: error: Certificate invalid: expired

AWS 上で Blessを構築

Bless のコードを取得

Bless のコードは GitHub で管理されています。

$ git clone https://github.com/Netflix/bless.git
$ cd bless
$ ls -F
AUTHORS            LICENSE            NOTICE             README.md          bless_client/      lambda_compile.sh* setup.cfg          tests/
ISSUES.md          Makefile           OSSMETADATA        bless/             bless_logo.png     requirements.txt   setup.py

コードの修正

残念ながら Netflix BLESSはアーカイブモードでメンテされていないため、現在は動かなくなっている箇所を修正します。

依存ライブラリビルド用コンテナイメージの変更

Lambda の 依存ライブラリは $ make lambda-deps を実行すると、Docker コンテナ経由でビルドされます。

修正なしにビルドすると、Lambda 実行時に以下のエラーが発生します。

{
  "errorMessage": "Unable to import module 'bless_lambda': /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /var/task/cryptography/hazmat/bindings/_rust.abi3.so)",
  "errorType": "Runtime.ImportModuleError",
  "stackTrace": []
}

イメージを amazonlinux:2 から lambci/lambda:build-python3.7 へと以下の通り変更します。

$ git diff Makefile
diff --git a/Makefile b/Makefile
index a50356d..ee44d45 100644
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,6 @@ compile:

 lambda-deps:
        @echo "--> Compiling lambda dependencies"
-       docker run --rm -v ${CURDIR}:/src -w /src amazonlinux:2 ./lambda_compile.sh
+       docker run --rm -v ${CURDIR}:/src -w /src lambci/lambda:build-python3.7 ./lambda_compile.sh

 .PHONY: develop dev-docs clean test lint coverage publish

marshmallow のバージョンのダウングレード

修正なしにライブラリをインストールすると、Lambda 実行時に以下のエラーが発生します。

{
  "errorMessage": "__init__() got an unexpected keyword argument 'strict'",
  "errorType": "TypeError",
  "stackTrace": [
    "  File \"/var/task/bless_lambda.py\", line 13, in lambda_handler\n    return lambda_handler_user(*args, **kwargs)\n",
    "  File \"/var/task/bless/aws_lambda/bless_lambda_user.py\", line 68, in lambda_handler_user\n    schema = BlessUserSchema(strict=True)\n"
  ]
}

marshmallow のバージョンを3未満に指定します。

$ git diff setup.py
diff --git a/setup.py b/setup.py
index f148b9d..61a6cb6 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ setup(
         'boto3',
         'cryptography',
         'ipaddress',
-        'marshmallow',
+        'marshmallow<3',
         'kmsauth'
     ],
     extras_require={

CA 証明書を指定

BLESS は CA の証明書発行プロセスを Lambda で置き換えるものです。

証明書発行にあたり、CA キーペアの秘密鍵が必要です。

秘密鍵、及び、秘密鍵のパスフレーズを bless/aws_lambda/ 以下で設定します。

CA 秘密鍵の展開

秘密鍵を bless/aws_lambda/ 以下にコピーします。

$ cp /path/to/bless-ca-  bless/aws_lambda/bless-ca-
$ chmod 444 bless/aws_lambda/bless-ca-

CA パスフレーズの管理

CAのキーペア作成時にパスフレーズを設定しました。

パスフレーズをKMSで暗号化して、設定ファイルで管理します。

まず、KMS の CMK を設定し、エイリアス alias/bless を設定します。

次に、パスフレーズを この鍵で暗号化します。

$ aws kms encrypt --key-id alias/bless --plaintext CA-KEYPAIR-PASSPHRASE
{
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT",
    "KeyId": "arn:aws:kms:ap-northeast-1:123:key/xxx-yyy-zzz",
    "CiphertextBlob": "AQICAH...Mvgi/g=="
}

base64 エンコードされた ciphertext CiphertextBlob を控えておきます。

サンプルの設定ファイルをコピーします。

$ cp ./bless/config/bless_deploy_example.cfg ./bless/aws_lambda/bless_deploy.cfg

./bless/aws_lambda/bless_deploy.cfg では

  • base64エンコードされたパスフレーズを リージョン_password で指定
  • CA 秘密鍵のパスを ca_private_key_file で指定

します。

$ diff -u ./bless/config/bless_deploy_example.cfg ./bless/aws_lambda/bless_deploy.cfg
--- ./bless/config/bless_deploy_example.cfg     2022-03-29 09:51:55.348261267 +0000
+++ ./bless/aws_lambda/bless_deploy.cfg 2022-03-29 16:33:06.903587774 +0000
@@ -30,10 +30,12 @@
 # for each aws region specify a config option like '{}_password'.format(aws_region)
 us-east-1_password = <INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
 us-west-2_password = <INSERT_US-WEST-2_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
+ap-northeast-1_password = AQICAH...Mvgi/g==
 # Or you can set a default password. Region specific password have precedence over the default
 # default_password = <KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
 # Specify the file name of your SSH CA's Private Key in PEM format.
-ca_private_key_file = <INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>
+ca_private_key_file = bless-ca-
 # Or specify the private key directly as a base64 encoded string.
 # ca_private_key = <INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>

Clone したコードとの変更は以下の通りです。

$ git status .
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   Makefile
        modified:   setup.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        bless/aws_lambda/bless-ca-
        bless/aws_lambda/bless_deploy.cfg

Lambda のデプロイ

準備は整ったので、Lambda をデプロイします。

# パッケージのビルド。事前に dockerd を起動
$ make lambda-deps

# Lambda コードの Zip化
$ make publish

$ ls publish/
bless_lambda bless_lambda.zip

# Lambda 関数の作成
$ aws lambda create-function \
  --function-name bless \
  --zip-file fileb://publish/bless_lambda.zip  \
  --handler bless_lambda.lambda_handler \
  --runtime python3.7 \
  --role arn:aws:iam::123:role/lambda_basic_execution \
  --timeout 300

BLESS を使った証明書ベースの SSH の確認

作成した BLESS で証明書を取得してみましょう。

  • ログインユーザー名
  • 接続元IPアドレス
  • 公開鍵

を JSON ファイルで用意します。

{
  "bastion_user": "ec2-user",
  "remote_usernames": "ec2-user",
  "bastion_user_ip": "172.31.17.236",
  "bastion_ips": "172.31.17.236",
  "command" : "",
  "public_key_to_sign": "ssh-rsa AAAAB... ec2-user@bless-client",
  "kmsauth_token": ""
}

この設定ファイル(特に公開鍵)を Lambda に渡すと、証明書を受け取れます。

$ aws lambda invoke \
  --invocation-type RequestResponse \
  --function-name bless \
  --payload fileb://input.json response.json
{
    "ExecutedVersion": "$LATEST",
    "StatusCode": 200
}

# レスポンスを確認
$ cat response.json
{
  "certificate": "ssh-rsa-cert-v01@openssh.com AAAAB...+GYZX3vLY3ulyA== ec2-user@bless-client"
}

# レスポンスから証明書を抜き出す
$ cat response.json | jq -r .certificate > bless-client-cert.pub

$ ls -1
bless-ca
bless-ca.pub
bless-client-cert.pub
bless-client.pub
input.json
response.json

# 証明書を確認
$ ssh-keygen -L -f bless-client-cert.pub
bless-client-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com user certificate
        Public key: RSA-CERT SHA256:RfvG/05VCKCC1946T0CrPIcJeKxpg0sL8B4kjbr7lxs
        Signing CA: RSA SHA256:NQblA7U2K61fl3bTt7Y2NHYSHy3XwaDH+xD7EhxqCh0
        Key ID: "request[eb6a3d8f-af1d-4652-89e7-a381f02351e0] for[ec2-user] from[172.31.34.109] command[] ssh_key[RSA ea:00:8f:ca:6d:30:e7:83:74:07:59:38:2e:2f:ad:57]  ca[arn:aws:lambda:eu-central-1:205974338614:function:bless] valid_to[2022/03/29 13:33:20]"
        Serial: 0
        Valid: from 2022-03-29T13:29:20 to 2022-03-29T13:33:20
        Principals:
                ec2-user
        Critical Options:
                source-address 172.31.34.109
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc


# ホストに接続
$ ssh -i bless-client ec2-user@172.31.13.229

無事、サーバーにログインできたでしょうか?

最後に

証明書ベースのSSH認証、及び、AWS上で証明書発行をAs a Service化した Netflix BLESS を紹介しました。

従来の公開鍵ベースの認証では、クライアントの公開鍵を認証先サーバーに展開する必要があり、鍵の管理が大変でしたが、証明書ベースの認証では、証明書を発行する CA キーペアの公開鍵が認証先サーバーに登録するだけで済みます。

証明書の発行処理をアクセスコントロールし、証明書の有効期限を十分に短くすれば、従来のSSH認証基盤の課題の多くが解決されるでしょう。

AWS には EC2 Instance Connect という EC2 ログイン認証方式があります。
ユーザーが公開鍵を登録すると、公開鍵に紐づく秘密鍵で短期間だけSSH認証可能で、SSH の証明書ベースの認証と発想は同じです(裏の実装も同じかもしれませんね)。

EC2のSSHアクセスをIAMで制御できるEC2 Instance Connectが発表されました | DevelopersIO

サーバーにSSHがインストールされている限り、オンプレ・クラウドに関係なく証明書認証ベースのSSHは使えます。

最近はサーバーにSSHする機会はめっきり減ってしまいましたが、たくさんのサーバーをお守りしている場合は、検討の余地があるかもしれません。

なお、OpenSSH証明書認証フレームワークの先鋭だった Netflix Bless はその役割を終え、現在は アーカイブモード です。

With the existence of more SSH certificate tools since the release of BLESS, and better SSH access management from AWS, we're moving BLESS to the archived OSS project state. This means we no longer plan to maintain the project, but will be keeping it public for others who may still use it.
https://github.com/Netflix/bless/blob/master/README.md

プロダクションでの利用はお控えください。

参考

https://speakerdeck.com/rlewis/how-netflix-gives-all-its-engineers-ssh-access-to-instances-running-in-production

https://www.youtube.com/watch?v=JwLGsWYVjqU

https://www.youtube.com/watch?v=kQkzzPiMnuQ

Discussion

ログインするとコメントできます