3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Japan AWS Top EngineersAdvent Calendar 2024

Day 1

AWSで承認フロー付きPrivateCAの仕組みを考えてみた

Last updated at Posted at 2024-11-30

はじめに

最近ではAWSでもmTLS通信に対応するサービスも増えており、クライアント証明書やサーバー証明書を発行する機会が多くなってきているように思います。
開発の時などは自己署名証明書を使用することが一般的かと思いますが、組織内に様々なチームが存在していた場合、その証明書の管理は非常に難しくなります。

image.png

たとえば自己署名証明書を使用したALBにTLS通信を行う場合、接続元はそのサーバ証明書が信頼のあるものかを検証するために、別途トラストストアに証明書を登録する必要があります。ALBごとに別の場所で証明書発行をしていた場合、各ALBごとに中間証明書をトラストストアに登録する必要がでてきてしまい、非常に扱いにくくなります。

管理観点で言えば、認証局の管理を一元的に行うことが望ましい場面が多くあるように思います。
(必ずしもそうでないこともありますが...)

ではこれをAWSサービスで解決しようと思うと、ACMのPrivate CAが挙げられると思います。
ただしこのサービスでは月額$400固定でかかってしまい、どうにもお財布には優しくないです...

そこで今回はコストも抑えつつ、AWS上に自作のPrivateCAの仕組みを作れないかを考えてみました!
せっかく考えるのであれば、承認フローを入れ込んだり、自動化もできないかなと思い、検討してみました。

※本記事は本番導入用ではなく、あくまで個人の検証によるブログ記事です。

やりたいこと

今回目指した姿は以下の通りです。
image.png

機能面

  • 申請ユーザーはCSRをブラウザから送信できること
    • 申請ユーザーの申請負荷を下げることと内部リソースを極力触らせないことが目的
  • 管理者によってCSRに対する署名承認/否認ができること
  • 申請ユーザーが受付連絡や完了連絡をメールで受け取ることができること
  • 申請ユーザーが証明書/中間CA証明書を受け取れること

非機能面

  • 可能なかぎりプライベートな接続をすること
  • 承認作業以外の部分は自動化されていること
  • 申請は頻繁には起こらないため、可能なかぎり稼働コストを抑えること
  • メールは外部で取得したドメインを使用し、no-reply@xxxx.xxxのようなメールアドレスからメールが届くこと

検討したアーキテクチャ

image.png

前提事項

  • 以下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に登録を行って利用する

以下は作成例

local PC terminal
openssl genrsa -out RootCA.key
openssl req -new -x509 -days 7 -key RootCA.key -out RootCA.crt -subj "//CN=RootCA"

①EC2

image.png

サーバー情報

簡易的なプロキシサーバとなるので、基本的には小さくても十分です。
検証では以下の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セッションマネージャーを使用してサーバーにログインします。
image.png

接続をしたら、まず初めにnginxをインストールして起動させます。

sudu dnf install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx

/etc/nginx/nginx.confに以下の設定を追記しました。後述のS3静的ホスティング設定やAPI Gatewayの設定が完了したのちに記載する項目を含んでいます。

/etc/nginx/nginx.conf
:
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に接続をし、ポートフォワーディングを行います。

クレデンシャルの登録は以下のコマンドで実施します。

local PC terminal
aws config --profile ssm-user
 # 取得したアクセスキーとシークレットアクセスキーを登録

EC2への接続は以下のコマンドで実施します。今回EC2のポート80に対してローカルPCのポート180を割り当てていきます。

local PC terminal
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を使用したシンプルビューにしました。

image.png

コードは以下の通りです。
https://github.com/SawaShuya/CSRSubmitPage

ホスティング設定

静的ホスティング資材を格納する新規バケットを作成し、資材を以下に格納します。

Bucket Name
├ index.html
├ JavaScript
    └ script.js
└ CSS
    └ styles.css

プロパティ > 静的ウェブホスティングから有効化を行います。
image.png

設定を行うとバケットウェブサイトエンドポイントが払い出されるため、前述の通りNginxへの適用を行います。

バケットポリシー

よりセキュアな接続にするため、バケットポリシーでVPCエンドポイントからのみからの通信に制限しました。

BucketPolicy
{
    "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エンドポイントからのみの通信に制限しました。

APIGatewayResourcePolicy
{
  "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で生成
email 文字列 申請者メールアドレス 申請者の入力値
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用ユーザーの作成を行います。
image.png

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
サブスクリプション アドレス 承認者のメールアドレス

トピックとサブスクリプションを登録すると、設定したメール宛に登録承認リクエストメールが届きます。こちらを忘れずにクリックをして登録を行います。

image.png

⑧SSM Automation

Systems ManagerのAutomationを使用して署名フローを作成していきます。

処理の流れ

① aws:approve アクションで承認リクエストメールを送信
② 承認者の承認待ち(最大7日)
② 承認されたらinveke:function

ロール設定

以下のアクションを許可するロールを作成します。セキュアになるようResourceは自分のアカウントに絞っています。

  • sns:Publish
  • lambda:InvokeFunction

コーディング

Systems managerコンソール > ドキュメント > ドキュメントの作成 > オートメーションから設定を行います。
image.png

以下のようにドキュメントを設定します

automation
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}}'

ビューで見ると以下のようになっていると思います。
image.png

補足:
メール本文に各パラメータの値を表示するため、Parameter属性を設定しています。
またInvokeFunctionではidの値をpayloadとして後述のLambda (署名用) を呼び出しています。

⑨Secrets manager

ルートCAの秘密鍵を格納します。
改行コード(\n)が含まれるように保存をしておきます。
image.png

⑩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/にアクセスし、必要な情報を入力します。

image.png

リクエストを送ると、内部ではLmabdaが実行され、申請者にはリクエスト完了メールが届きます。
image.png

同時に承認者に対して承認リクエストメールが送付されます。
image.png

メール内のリンクを踏むと承認/否認ができる画面に遷移をするため、承認を選んでSubmitします。
image.png

内部では署名用のLambdaが実行され、署名が完了すると署名済み証明書とRootCA証明書がzipファイルとして申請者にメールで送付されます。
image.png

DynamoDBを確認しても対象の証明書情報は登録がされています。
image.png

S3を確認しても署名済み証明書は保管されています。
image.png

次に送付された証明書が使えるかどうかを実際に確認します。
ACMにて証明書と秘密鍵を入力し、インポートを行います。
image.png

単純構成のALB-Lambdaを構成し、ACMをアタッチしてHTTPS通信ができるか確認します。
image.png

image.png

通信可能なインスタンスから、通信をしてみると正しく通信することを確認することができました。

curl --cacert RootCA.crt https://<ALBのAレコード>

image.png

ちなみにcacertを設定しなかった場合には、curlでチェーンのエラーが発生します。
image.png

おわりに

以上で承認フロー付きPrivateCAの構築でした!
まだまだ改修の余地はありますが、サーバレスを活用しながら、コストを抑えた構成が実現できました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?