お久しぶりです、エムスリーエンジニアリンググループ 兼 QLife エンジニアの園田です。
STNS という pam と連携可能な HTTP プロトコルを利用した Linux の認証機構があります。
通常は TOML でアカウント情報を管理するのですが、API のインターフェースが公開されているため、独自のバックエンドサーバーを構築して利用することも可能です。
弊社 SRE の寺岡も独自のバックエンドを実装した記事を Qiita に投稿しています。
今回実装した STNS のバックエンドは、AWS の API Gateway + Lambda + DynamoDB で実現しました。
ただ、この構成もすでに既出記事がいくつかあり、そんなに珍しくもありません。
今回の実装のポイントは、API Gateway の Private エンドポイントで実装していて、特定の VPC や VPC エンドポイント経由でしか利用できないようにしていることです。
VPC エンドポイントとは、VPC 内から VPC 外のリソースにアクセスする際に、 Private IP が割り当てられた名前解決可能なエンドポイントです。API Gateway での VPC エンドポイントの利用については AWS のドキュメントをご覧ください。
AWS リソースの構築は Terraform を利用します。当初 SAM (Serverless Application Model) での実装を試みたのですが、 Private エンドポイントで必要になる API Key が STNS のリクエスト形式と一致しなかったため SAM での構築は諦め、Custom Authorizer に API Key の処理をさせることでむりやりインターフェースをあわせました。
なお、 Custom Authorizer を利用した副次的効果として、サーバーごとに認証を許可するアカウントやグループを制御することが可能になりました。
ごちゃごちゃ書いていますけど、まとめると、
- VPC 内 または VPC エンドポイント経由でしかアクセスできない STNS のバックエンド API をサーバーレスで実現した。
- Custom Authorizer により、サーバーごとに認証を許可するアカウントやグループを制御できるようにした。
ということです。
Terraform のソースは Github に置いてあります。
使い方
AWS リソースの構築
バックエンド API を AWS に構築します。以下、モジュール利用の例です。
provider "aws" { region = "ap-northeast-1" version = "1.59.0" } variable "stns_allowed_vpce_id" { description = "STNS API にアクセスを許可する VPC エンドポイント ID" } module "stns-backend" { source = "github.com/QLife-Inc/aws-stns-serverless-api" api_policy_json = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": "*", "Action": "execute-api:Invoke", "Resource": "execute-api:/*/*/*", "Condition": { "StringNotEquals": { "aws:sourceVpce": "${var.stns_allowed_vpce_id}" } } }, { "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke", "Resource": "execute-api:/*/GET/*" } ] } EOF } output "stns_api_endpoint" { value = "${module.stns-backend.endpoint_url}" }
以下は、terraform.tfvars の例です。
stns_allowed_vpce_id = "vpce-xxxxx"
リソースポリシーの自由度を優先しているため、リソースポリシー全体をパラメータとして渡せるようにしています。そのため、VPCエンドポイントIDではなく、VPC ID を利用することも可能です。
設定ができたら、 terraform get
もしくは terraform init
で Github のソースをローカルにダウンロードし、 terraform plan
, terraform apply
します。
Terraform により、以下のリソースが生成されます。
- DynamoDB テーブル 3 つ。ユーザー、グループ、認可トークン の各テーブル。
- Lambda 関数 2 つ。STNS の API 実装(Sinatra)と Custom Authorizer。
- API Gateway の Rest API および API Gateway の関連リソース(リソース定義, メソッド定義, デプロイメントや API Key, 使用量プランなど)。
- IAM ロール 3 つ。 Lambda の実行ロール 2 つと、API Gateway から Custom Authorizer を実行するための Invocation ロール。
VPC エンドポイントは作成されません。必要に応じて作成してください。
apply
コマンドを実行すると、以下のように Outputs に API Gateway のエンドポイントが出力されます。
Outputs: stns_api_endpoint = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v2
DynamoDB データ登録
残念ながら今のところ API は参照のみで、DynamoDB へのデータ登録はマネジメントコンソールや CLI を利用して実施する必要があります。
まず、アカウントのグループを作成します。デフォルトでは、 stns-groups
というテーブルが生成されているので、そこに登録します。
STNS の link_groups
もサポートしています。
aws dynamodb put-item \ --table-name "stns-groups" \ --item '{ "name": {"S": "my-group"}, "id": {"S": "2001"}, "users": {"L": [ {"S": "oreno-user"} ]}, "link_groups": {"L": [] } }'
続いてユーザーを登録します。なお、RDBMS と違い参照制約はないため、データの登録順序は不問です。STNS の link_users
もサポートしています。
aws dynamodb put-item \ --table-name "stns-users" \ --item '{ "name": {"S": "oreno-user"}, "id": {"S": "2001"}, "group_id": {"S": "2001"}, "shell": {"S": "/bin/bash"}, "directory": {"S": "/home/oreno-user"}, "keys": {"L": [ { "S": "ssh-rsa AAAAA ........"} ]}, "link_users": {"L": [] } }'
DynamoDB のユーザーテーブルとグループテーブルは基本的に STNS の API インターフェースにおけるレスポンスの JSON スキーマとほぼ同じです。
違いは、 link_users
と link_groups
フィールドがあればそれを解釈できるくらいです。
最後に、Lambda の認可を行うためのトークンを登録します。この例では、ここで登録したトークンを利用するサーバーで、上記で作成したグループ以外のログインを認めないことにします。
この認可トークンテーブルだけは STNS の仕様にない独自のものなので、詳細は README やソースをご覧ください。
aws dynamodb put-item \ --table-name "stns-authorizations" \ --item '{ "token": {"S": "oreno-my-stns-token-001"}, "description": {"S": "my-group にだけ認証を許可するトークン"}, "groups": {"L": [ {"S": "my-group"} ]}, "users": {"L": []} }' # groups や users フィールドがなければ、すべての登録ユーザーとグループにログインを許可します
クライアントセットアップ
最後にクライアントの設定です。構築した STNS API にアクセス可能な VPC 内に EC2 インスタンスを構築します。最初のセットアップは ec2-user
などで行います。
VPC エンドポイントの利用には VPC の DNS サポートを有効にするなどいくつかの制約があるので AWS のドキュメントで確認してください。
クライアントの設定は、STNS ドキュメントの通りで問題ないですが、 stns-v2
をインストールする必要はありません。libnss-stns-v2
のみのインストールで大丈夫です。
なお、Amazon Linux 2 の場合はもともと独自の認証方法を利用するために SSH のコマンドが用意されているため、設定手順が他と異なります。Amazon Linux 2 の場合の設定方法については後述します。
設定のポイントは、 /etc/stns/client/stns.conf
の api_endpoint
と auth_token
を書き換えることです。
api_endpoint = "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v2" auth_token = "oreno-my-stns-token-001"
これで、上記クライアント設定を行った EC2 インスタンスに対して、ユーザーテーブルに登録したアカウント名と公開鍵に対する秘密鍵でのログインが可能になります。
ssh -i "/path/to/keysに登録した公開鍵に対する秘密鍵" \ stns-usersに登録したユーザー名@サーバーホスト
Amazon Linux 2 で試しましたが、ログインにかかる時間は全く気にならない速度でした。
ちなみに、 CentOS 6 でも試したのですが、ルート証明書が古くて SSL のハンドシェイクができず、リトライを繰り返して SSL Verify を諦めてからログインするため、ログインに 3 秒くらいかかりましたw
Amazon Linux 2 のクライアント設定
そもそも、 STNS ドキュメント通りだと yum でのインストールができないので、rpm からインストールします。 https://repo.stns.jp/centos/x86_64/7 から rpm の URL をコピーしておきます。
sudo yum localinstall -y https://repo.stns.jp/centos/x86_64/7/libnss-stns-v2-2.2.0-1.x86_64.el7.rpm
/etc/nsswitch.conf
は STNS ドキュメントの通りで、 stns
を追加するだけです。
- passwd: sss files - shadow: files sss - group: sss files + passwd: sss files stns + shadow: files sss stns + group: sss files stns
STNS ドキュメントにある通り、 /etc/pam.d/sshd
でログイン時にホームディレクトリが作成されるようにしておきます。
echo 'session required pam_mkhomedir.so skel=/etc/skel/ umask=0022' >> /etc/pam.d/sshd
最後に /etc/ssh/sshd_config
の変更です。これがポイントですが、 Amazon Linux 2 ではもともと独自の AuthorizedKeysCommand
が指定されています。
そのため、もともと指定されているコマンドをさらにラップしたコマンドを用意します。
#!/bin/bash # まずは STNS での公開鍵取得を試みる pubkeys=$(/usr/lib/stns/stns-key-wrapper $1) # 失敗したらもともと指定されていた Amazon Linux 2 用のコマンドにフォールバック if [ $? -ne 0 ]; then /usr/bin/timeout 5s /opt/aws/bin/curl_authorized_keys $1 $2 else echo "$pubkeys" fi
上記ファイルを /usr/local/bin/ssh-chain-wrapper
といったファイル名にして chmod 755
し、 /etc/ssh/sshd_config
を以下のように書き換えました。
- AuthorizedKeysCommand /usr/bin/timeout 5s /opt/aws/bin/curl_authorized_keys %u %f + AuthorizedKeysCommand /usr/local/bin/ssh-chain-wrapper %u %f
AuthorizedKeysCommandUser
は ec2-instance-connect
のままで問題ありません。
/etc/ssh/sshd_config
を書き換えたら sudo systemctl restart sshd.service
で sshd を再起動すれば設定完了です。
なお、ラッパーを作成せずに STNS の公式ドキュメント通りに設定しても STNS 上のアカウントでログインできますし、
ec2-user
でのログインもできました。 ただ、このラッパーが何を目的としているものなのかググっても出てこなかったため、念の為ラッパーファイルを作成したほうがいいでしょう。
2019/07/08 追記
2019/06/27 に発表された EC2 Instance Connect を利用している場合、もともとのコマンドは /opt/aws/bin/eic_run_authorized_keys
になっていると思われます。
その場合は上記スクリプトのフォールバック先のコマンドを eic_run_authorized_keys
に書き換えてあげれば動くと思います。
実装解説
Custom Authorizer
ほぼ素の Ruby で書いています。利用している gem は aws-record
のみです。 aws-record
は Aws::DynamoDB::Client
よりもちょっとだけ高レベルなAPIを提供する AWS 製の gem です。
STNS クライアントから auth_token
で指定したトークンが Authorization ヘッダで渡されるので、それをもとに DynamoDB の認可トークンテーブルを検索し、トークンごとの認可を取得しています。存在しないトークンだった場合は戻り値のポリシーの Effect
を Deny
にすることでクライアント側に 403 を返します。
取得した認可情報は戻り値の context
フィールドに格納することで、API 本体の Lambda に渡されるイベントペイロードに含めることができます。
以下、認可を許可する場合のレスポンスの例です。このレスポンスは、クライアントではなく API Gateway の Lambda 統合プロキシに渡されます。
{ principalId: auth_token, policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke' Effect: 'Allow', Resource: "arn:aws:execute-api:#{AWS_REGION}:#{AWS_ACCOUNT_ID}:#{API_ID}/*/GET/*" } ] }, usageIdentifierKey: ENV['API_KEY'], context: { token: auth_token, users: authz_context.users || [], groups: authz_context.groups || [], } }
ここでのポイントは、認可不可だった場合は Resource
のパスを /*/*/*
で、許可だった場合は /*/GET/*
で返すことです。
不可だった場合に /*/GET/*
で返してしまうと、 POST や DELETE ができることになってしまうためです。(現状は GET しかありませんが)
STNS API (Sinatra)
API サーバーは Ruby の Sinatra で書いています。Lambda で Sinatra を実行するためのサンプルが Github の aws-samples にあったので、Lambda で受けて Sinatra にイベントを渡す処理はほぼ流用しています。
エンドポイントは STNS のインタフェース上、 /users
と /groups
の 2 つのみで、それぞれ DynamoDB のテーブルを参照して、JSON を返しているだけです。
唯一、認可の処理だけは Custom Authorizer から渡された認可コンテキスト(認可トークンテーブルの内容)をもとに返すユーザーやグループをフィルタリングしています。
Custom Authorizer からのレスポンスに context
フィールドを含めることで、API Gateway の Lambda 統合によるイベントペイロードの $.requestContext.authorizer
という JSON パスに、渡した内容が格納されます。
なので、 lambda_handler
で雑に ENV
に設定して、Sinatra 側で受け取っています。
def lambda_handler(event:, context:) ENV['AUTHORIZATION_CONTEXT'] = (event.dig('requestContext', 'authorizer') || {}).to_json # ... end
それ以外はごく普通の API アプリなので特筆することはないです。
Terraform + Lambda
Lambda を Terraform で扱うためにはいろんなハックがあると思いますが、こちらでは external
データソース(外部コマンド結果のデータソース)を使って泥臭くアーカイブしてアップロードファイルを生成しています。そのため、この Terraform を実行するために zip や md5, find といった Linux 系のコマンドが必要になります。Windows で実行する場合はご注意ください。
そのままだと毎回 bundle install
されるので、 *.rb
や Gemfile
などのソースファイルの md5 をローカルファイルに出力し、前回実行時と変更があった場合のみアーカイブを再作成するようにしています。正直ここが一番面倒でしたw
参考URL
- STNSの独自サーバーを書いてみた - Qiita
- API Gateway の Lambda プロキシ統合をセットアップする - Amazon API Gateway
- https://code.i-harness.com/ja/docs/terraform/providers/aws/guides/serverless-with-aws-lambda-and-api-gateway
- Linuxユーザ管理の決定版? 〜STNSとサーバレスで夢が広がる〜【cloudpack大阪ブログ】 - Qiita
- GitHub - aws-samples/serverless-sinatra-sample: Demo code for running Ruby Sinatra on AWS Lambda
エンジニア募集!!
QLife では共にチーム一丸となってより良いものづくりにこだわれる仲間を募集中です! 小さいサービスが多いので新しい AWS のサービスの利用にも非常に積極的に取り組んでいます! カジュアル面談も行っていますので、興味がある方は entry@qlife.co.jp に「カジュアル面談希望」とメールをください!
エムスリーでもエンジニアを随時募集しています!共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です! AWS 以外にも GCP や Firebase などのクラウドも活用しています!興味がある方はカジュアル面談やTechtalkにおこしください!!