はじめに
最近ではAWSでもmTLS通信に対応するサービスも増えており、クライアント証明書やサーバー証明書を発行する機会が多くなってきているように思います。
開発の時などは自己署名証明書を使用することが一般的かと思いますが、組織内に様々なチームが存在していた場合、その証明書の管理は非常に難しくなります。
たとえば自己署名証明書を使用したALBにTLS通信を行う場合、接続元はそのサーバ証明書が信頼のあるものかを検証するために、別途トラストストアに証明書を登録する必要があります。ALBごとに別の場所で証明書発行をしていた場合、各ALBごとに中間証明書をトラストストアに登録する必要がでてきてしまい、非常に扱いにくくなります。
管理観点で言えば、認証局の管理を一元的に行うことが望ましい場面が多くあるように思います。
(必ずしもそうでないこともありますが...)
ではこれをAWSサービスで解決しようと思うと、ACMのPrivate CAが挙げられると思います。
ただしこのサービスでは月額$400固定でかかってしまい、どうにもお財布には優しくないです...
そこで今回はコストも抑えつつ、AWS上に自作のPrivateCAの仕組みを作れないかを考えてみました!
せっかく考えるのであれば、承認フローを入れ込んだり、自動化もできないかなと思い、検討してみました。
※本記事は本番導入用ではなく、あくまで個人の検証によるブログ記事です。
やりたいこと
機能面
- 申請ユーザーはCSRをブラウザから送信できること
- 申請ユーザーの申請負荷を下げることと内部リソースを極力触らせないことが目的
- 管理者によってCSRに対する署名承認/否認ができること
- 申請ユーザーが受付連絡や完了連絡をメールで受け取ることができること
- 申請ユーザーが証明書/中間CA証明書を受け取れること
非機能面
- 可能なかぎりプライベートな接続をすること
- 承認作業以外の部分は自動化されていること
- 申請は頻繁には起こらないため、可能なかぎり稼働コストを抑えること
- メールは外部で取得したドメインを使用し、no-reply@xxxx.xxxのようなメールアドレスからメールが届くこと
検討したアーキテクチャ
前提事項
- 以下VPCエンドポイントが作成されており、ネットワーク(ルートやセキュリティグループ)設定も完了していること
エンドポイントタイプ | サービス名 |
---|---|
Gateway | com.amazonaws.ap-northeast-1.s3 |
com.amazonaws.ap-northeast-1.dynamodb | |
Interface | com.amazonaws.ap-northeast-1.execute-api |
com.amazonaws.ap-northeast-1.ssm | |
com.amazonaws.ap-northeast-1.ssmmessages | |
com.amazonaws.ap-northeast-1.ec2messages | |
com.amazonaws.ap-northeast-1.email-smtp | |
com.amazonaws.ap-northeast-1.secretsmanager |
セキュリティグループはインバウンドとしてPrivateリソースに付与するセキュリティグループからの通信のみを許可しています。
- 今回認証局についてはルート認証局のみを利用し、中間認証局を作らないこと
- 認証局は事前に作成しておき、Secrets mnagerに登録を行って利用する
以下は作成例
openssl genrsa -out RootCA.key
openssl req -new -x509 -days 7 -key RootCA.key -out RootCA.crt -subj "//CN=RootCA"
①EC2
サーバー情報
簡易的なプロキシサーバとなるので、基本的には小さくても十分です。
検証では以下のEC2を利用しました。
設定項目 | 設定 |
---|---|
プラットフォーム | Amazon Linux |
インスタンスタイプ | t2.micro |
ボリューム | 8 Gib |
サブネット | プライベートサブネット |
ロール | 「AmazonSSMManagedInstanceCore」を含んだロール |
セキュリティグループ | セキュリティグループのインバウンドルールは設定なし。アウトバウンドはVPC内への通信を許可する |
プロキシ設定 (Nginx)
Nginxをリバースプロキシサーバーとして利用しました。以下のようなリクエストの振り分けができるようにnginxを設定していきます。
HTTPメソッド | パス | ルーティング |
---|---|---|
GET | /csr-request | S3静的ホスティングURL |
POST | /submit-csr-request | CSR申請先 API Gateway エンドポイント |
コンソールの「接続」からSSMセッションマネージャーを使用してサーバーにログインします。
接続をしたら、まず初めにnginxをインストールして起動させます。
sudu dnf install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
/etc/nginx/nginx.conf
に以下の設定を追記しました。後述のS3静的ホスティング設定やAPI Gatewayの設定が完了したのちに記載する項目を含んでいます。
:
http {
:
:
log_format proxy_log '$remote_addr - $remote_user [$time_local]'
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"'
'to: "$proxy_host$request_uri"';
server {
listen 80;
server_name localhost;
location /csr-request/ {
proxy_pass <S3静的ホスティング URL>/;
access_log /var/log/nginx/proxy_access.log proxy_log;
}
location /submit-csr-request/ {
proxy_pass <API Gateway URL>/;
access_log /var/log/nginx/proxy_access.log proxy_log;
}
}
}
注意
locationやproxy_pathのURLの最後には"/"を入れるようにしてください。
CF.
nginxの設定ファイル検証を行い、問題がないか確認します。
sudo nginx -t
問題なければnginxを再起動します。
sudo systemctl restart nginx
SSMセッションマネージャーを使用したポートフォワーディング設定
SSMセッションマネージャーを使用することで、セキュリティグループを解放することなくプライベートサブネット内のサーバーにアクセスすることができます。今回はEC2のサーバに対してポートフォワードを行い、HTTPでアクセスを行います。
SSM用IAMユーザーを作成
まず、ローカルPCからAWS CLIでStart-sessionが使用できる用に専用のユーザーを作成します。
(Start-Sessionさえ使えればいいので、ロールをアタッチしてスイッチでも問題はないです)
権限は以下を付与します。
- ssm:ResumeSession
- ssm:TerminateSession
- ssm:StartSession
接続
SSM用IAMユーザーのクレデンシャルと、AWS CLIを使用してEC2に接続をし、ポートフォワーディングを行います。
クレデンシャルの登録は以下のコマンドで実施します。
aws config --profile ssm-user
# 取得したアクセスキーとシークレットアクセスキーを登録
EC2への接続は以下のコマンドで実施します。今回EC2のポート80に対してローカルPCのポート180を割り当てていきます。
aws ssm start-session --target <YOUR INSTANCE ID> --document-name AWS-StartPortForwardingSession --parameters '{\"portNumber\":[\"80\"],\"localPortNumber\":[\"180\"]}' --profile ssm-user
レスポンスとして以下がターミナルに表示されていればポートフォワーディングは完了です。
Starting session with SessionId: xxxxxxxxxxxxxxx
Port 180 opened for sessionId xxxxxxxxxxxxxxxx
Waiting for connections...
Waiting for connections...
が評された状態でブラウザを開き、http://localhost:180/
を開くとEC2にアクセスできる状態になります。
②S3
静的ホスティング
コーディング
フロントエンドのページを作っていきます。ランニングコストが低いことや、単純なGUIで十分であることから、HTML/CSS/Javascriptを使用したシンプルビューにしました。
コードは以下の通りです。
https://github.com/SawaShuya/CSRSubmitPage
ホスティング設定
静的ホスティング資材を格納する新規バケットを作成し、資材を以下に格納します。
Bucket Name
├ index.html
├ JavaScript
└ script.js
└ CSS
└ styles.css
プロパティ > 静的ウェブホスティングから有効化を行います。
設定を行うとバケットウェブサイトエンドポイントが払い出されるため、前述の通りNginxへの適用を行います。
バケットポリシー
よりセキュアな接続にするため、バケットポリシーでVPCエンドポイントからのみからの通信に制限しました。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAccessFromSpecificVPCE",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::csr-request-page/*",
"Condition": {
"StringEquals": {
"aws:SourceVpce": "<VPCエンドポイントID>"
}
}
}
]
}
③API Gateway
構成
シンプルにLambdaを呼び出す構成としました。
payloadはフロントエンドから送られてくるJSONをそのままLambdaに流します。
リソースベースポリシー
こちらもリソースベースポリシーとして、VPCエンドポイントからのみの通信に制限しました。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:ap-northeast-1:820356253015:x3gnl2o7tj/*",
"Condition": {
"StringEquals": {
"aws:sourceVpce": "VPCエンドポイントID"
}
}
}
]
}
④DynamoDB
以下のDynamoDBテーブルを作成します。ほかの項目については全てデフォルトとしています。
テーブル設定
設定項目 | 設定値 |
---|---|
テーブル名 | certificates |
タイプ | オンデマンド |
各種カラム設定
カラム名 | 型 | 説明 | 備考 |
---|---|---|---|
id (PK) | 文字列 | 識別ID | DynamoDB登録時にLambdaで生成 |
文字列 | 申請者メールアドレス | 申請者の入力値 | |
comment | 文字列 | 申請者からのコメント | 申請者の入力値 |
csrContent | 文字列 | 申請者からのCSRファイルの中身の値 | 申請者の入力値 |
registrationDate | 文字列 | 申請日時 | DynamoDB登録時にLambdaで生成 |
serial | 文字列 | 発行証明書のシリアルナンバー | 証明書署名時にLambdaで生成 |
signedDate | 文字列 | 署名日時 | 署名所署名時にLambdaで生成 |
⑤SES
SESを使用して申請者に対してメールを送付します。
SESは事前に登録していないユーザーに対してもメールを送ることができ、またno-reply@<任意ドメイン>
のようなメールアドレスからメールを送付することが可能です。
ただしSESではデフォルトでサンドボックス利用モードになっており、登録したアドレスにしかメールが遅れないようになっているため、検証ではメールアドレスも登録を行います。
またSESはVPCエンドポイントを介した利用の場合、SMTPサーバとしての利用に制限されます。SMTP認証可能なID+PASSを払い出していきます。
ドメイン登録
※ Route53やCloudFlare、お名前.comなどで自分のドメインを所有している必要があります。
設定方法は以下をご参照ください。
メールアドレス登録
自分用のメールアドレスを登録します。
設定を行うと承認メールが届くため、メールから承諾をします。
SMTP用ユーザー作成
後述しますが、SESはVPCエンドポイントを介した接続の場合、SMTPサーバーとして機能します。(SDKからの呼び出しに非対応です)そのため、SMTPサーバーにログインするためのユーザー名とパスワードを作成する必要があります。
SMTPユーザーの認証情報をパラメータストアに保存
SMTPの認証情報はSystems Manager パラメータストアで管理します。認証情報はSecure Stringを使用してデータの暗号化が行われるようにします。
⑥Lambda (CSR受付用)
処理の流れ
①入力値のチェック
メールアドレスやCSRファイルのフォーマットを確認します。
②DynamoDBへの登録
UUIDを生成します。本承認プロセスではこのIDをキーとして処理が進んでいきます。
UUID、登録日時、各入力値をDynamoDBに格納します。
③SESを介して申請者へのメールの送信
SESはプライベート通信下では、Python SDKを使用したメール送信には対応していません。(2024/11月地点)
SESVPCエンドポイントに対して、SMTPリクエストを送信する形をとることで、プライベート環境からでもメールを送信することができます。
④承認依頼用のSSM Automationの呼び出し
後述のSSm Automationを呼び出します。
コーディング
以下のコードをデプロイしました。
https://github.com/SawaShuya/SubmitCSRRequest
ロール設定
以下の権限の付与が必要です。ima:passRoleはAutomation実行ロールに対して権限を払い出しています。そのほかのリソースもアカウント内リソースあるいは特定のテーブルに対しての権限に絞っています。
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- ec2:CreateNetworkInterface
- ec2:DeleteNetworkInterface
- ec2:DescribeNetworkInterfaces
- dynamodb:PutItem
- dynamodb:GetItem
- ssm:GetParameters
- iam:PassRole
- ssm:StartAutomationExecution
その他設定
- タイムアウト:1分
- VPC配置
⑦SNS
後述の通り、Auromationのメール送信にはSNSトピックが必要です。
トピックとサブスクリプションを作成し、それらの紐づけを行います。
トピック・サブスクリプション設定
リソース | 項目 | 設定値 |
---|---|---|
トピック | トピック名 | 接頭辞として"Automation" を含むようにする。ex)"AutomationTopic" |
タイプ | standard | |
サブスクリプション | アドレス | 承認者のメールアドレス |
トピックとサブスクリプションを登録すると、設定したメール宛に登録承認リクエストメールが届きます。こちらを忘れずにクリックをして登録を行います。
⑧SSM Automation
Systems ManagerのAutomationを使用して署名フローを作成していきます。
処理の流れ
① aws:approve アクションで承認リクエストメールを送信
② 承認者の承認待ち(最大7日)
② 承認されたらinveke:function
ロール設定
以下のアクションを許可するロールを作成します。セキュアになるようResourceは自分のアカウントに絞っています。
- sns:Publish
- lambda:InvokeFunction
コーディング
Systems managerコンソール > ドキュメント > ドキュメントの作成 > オートメーションから設定を行います。
以下のようにドキュメントを設定します
schemaVersion: '0.3'
description: invoke Signiture CSR Lambda with approval
assumeRole: '{{AssumeRole}}'
parameters:
AssumeRole:
description: (Required) Automation Role.
type: String
default: <作成したロールARN>
allowedValues:
- <作成したロールのARN>
SnsTopic:
description: (Required) The SNS topic ARN used to send pending approval notification.
type: String
default: <作成したSNSトピックのARN>
allowedValues:
- <作成したSNSトピックのARN>
LambdaFunctionName:
description: (Required) Lambda Function name
type: String
default: <Lambda (署名用)の関数名>
allowedValues:
- <Lambda (署名用)の関数名>
RequestID:
description: (Required) Request ID which assigned in saving dynamodb
type: String
RequesterComment:
description: Comment from requester
type: String
RequesterEmail:
description: (Required) Requester's email address
type: String
CSRContent:
description: (Required) Content from Requester
type: String
mainSteps:
- name: approve
action: aws:approve
nextStep: invokeLambda
isEnd: false
onFailure: Abort
inputs:
NotificationArn: '{{SnsTopic}}'
Message: 'Approval required to Signiture ID: {{RequestID}}} RequesterEmail: {{RequesterEmail}} RequesterComment: {{RequesterComment}}'
MinRequiredApprovals: 1
Approvers:
- <承認者のIAMユーザー or ロールのARN>
- name: invokeLambda
action: aws:invokeLambdaFunction
maxAttempts: 3
timeoutSeconds: 300
isEnd: true
onFailure: Abort
inputs:
FunctionName: '{{LambdaFunctionName}}'
InputPayload:
id: '{{RequestID}}'
補足:
メール本文に各パラメータの値を表示するため、Parameter属性を設定しています。
またInvokeFunctionではidの値をpayloadとして後述のLambda (署名用) を呼び出しています。
⑨Secrets manager
ルートCAの秘密鍵を格納します。
改行コード(\n)が含まれるように保存をしておきます。
⑩S3 (証明書格納用)
ルートCAの証明書を格納しておきます。こちらは最終的にはユーザーへ送信するため、ファイルとしてS3に保存しておきます。
Bucket Name
├ intermediate-certificate/
└ <RootCAの証明書>
└ signed-certificate/
└ <シリアル番号>
└ <署名済み証明書>
⑪Lambda (署名用)
処理の流れ
①idからCSRの取得
DynamoDBに格納していたCSR情報を取得します。
②SecretsManagerからルートCAの秘密鍵を取得
Secrets Managerに登録した秘密鍵情報を取得します。
③S3からルートCA証明書を取得
Secrets Managerに登録した証明書情報を取得します。
④署名
ユーザーから送付されたCSR、ルートCA証明書、秘密鍵を使用して署名を行います。署名にはpythonのライブラリである、cryptographyを使用します。有効期限は1年としています。シリアル番号をランダムで割り振っています。
⑤DynamoDBに情報追加
DyanmoDBに追加情報として、シリアル番号と署名日時情報を保存します。
⑥証明書のファイル化&zip圧縮
署名済み証明書とルートCA証明書はLambda関数の/tmp
に一時的にファイルとして書き込みます。書きこみを行った後に、両ファイルを含んだディレクトリでzip圧縮を行います。また、署名済み証明書はS3(証明書格納用)に送信します。
③SESを介して申請者へのメールの送信
署名済み証明書とルートCA証明書のzipファイルを申請者に送付します。
コーディング
以下のコードをデプロイしました。
https://github.com/SawaShuya/SignitureCSR
ロール設定
以下の権限の付与が必要です。ima:passRoleはAutomation実行ロールに対して権限を払い出しています。そのほかのリソースもアカウント内リソースあるいは特定のテーブルに対しての権限に絞っています。
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- ec2:CreateNetworkInterface
- ec2:DeleteNetworkInterface
- ec2:DescribeNetworkInterfaces
- dynamodb:UpdateItem
- dynamodb:GetItem
- ssm:GetParameters
- s3:PutObject
- s3:GetObject
- secretsmanager:GetSecretValue
その他設定
- タイムアウト:1分
- VPC配置
動作確認
ここまでのシステムを一気通貫して動作確認を行います。
まずはローカルでCSRファイルを作成します。
その時にCommon Name(CN)は*.ap-northeast-1.elb.amazonaws.com
を設定します。
ローカルPCでSSMセッションマネージャーのポートフォワーディングを行った状態で、loccalhost:180/csr-request/
にアクセスし、必要な情報を入力します。
リクエストを送ると、内部ではLmabdaが実行され、申請者にはリクエスト完了メールが届きます。
メール内のリンクを踏むと承認/否認ができる画面に遷移をするため、承認を選んでSubmitします。
内部では署名用のLambdaが実行され、署名が完了すると署名済み証明書とRootCA証明書がzipファイルとして申請者にメールで送付されます。
DynamoDBを確認しても対象の証明書情報は登録がされています。
次に送付された証明書が使えるかどうかを実際に確認します。
ACMにて証明書と秘密鍵を入力し、インポートを行います。
単純構成のALB-Lambdaを構成し、ACMをアタッチしてHTTPS通信ができるか確認します。
通信可能なインスタンスから、通信をしてみると正しく通信することを確認することができました。
curl --cacert RootCA.crt https://<ALBのAレコード>
ちなみにcacertを設定しなかった場合には、curlでチェーンのエラーが発生します。
おわりに
以上で承認フロー付きPrivateCAの構築でした!
まだまだ改修の余地はありますが、サーバレスを活用しながら、コストを抑えた構成が実現できました。