はじめに
こんにちは!SREを担当してます上平と申します。
このエントリーではスマートバンク内部の業務で使われるシステムを構築した際の話を苦労した内容や学びを含めてご紹介したいと思います!
我々のようなカード発行業者はカード番号からユーザーを特定する業務があります。
この業務を効率化し、よりセキュアに担当者が業務を実施できるように管理画面を構築する必要がありました。また、このシステムはPCI DSSに準拠するように構築する必要があります。
今回は構築する際の技術選定や問題点についてご紹介し、これから同じようなシステムを構築される方の参考になれば幸いです。
PCI DSSとは
PCI DSSとはクレジットカード業界のセキュリティ基準のことで詳しくは以下の記事も御覧ください!
技術選定の観点
まず技術選定を行う上で、我々がやりたかった観点と、PCIDSSに準拠するために必要な観点を洗い出しました。
- フロントエンドはReactでバックエンドはgolangで実装したかった
- サーバーサイド開発工数を最小にしたかった
- PCI DSS要件としてMFAを導入する必要があった
- PCI DSS要件として操作LOGを残す必要があった(いつ誰が何をした)
- セキュアに構築する必要があるため、接続はPrivateのネットワークで構築したかった
最終構成に至るまでの試行錯誤
最終構成に至るまでの流れをざっくりとお話したいと思います。
PCI DSSに準拠する必要があったため、構成をfixするのに右往左往しました。
我々が実現しかった観点を踏まえて試行錯誤した内容をお話します。
試行錯誤構成No.1
API Gateway + Cognito
- API GatewayとCognitoを使って認証を実現する構成を考えました。
- 採用理由はAPI GatewayのLOGを簡単な設定で残せそうだったからです。
- Cognitoの採用理由はMFAの導入が簡単だったからです。
- docs.aws.amazon.com
試行錯誤構成No.2
ALB+Cognito
- ALBとCognitoを使って認証を実施する構成を考えました。
- 採用理由は単純にAPI Gateway+Cognitoでは実現不可能と判断したためALBを検討する事になりました。
- Cognitoの採用理由はMFAの導入が簡単だったからです。
最終構成
golang+Cognito
結局サーバーサイドの工数を拝借してgolangでCognito認証を実現する構成になりました。
不採用理由と学び
各試行錯誤がなぜだめだったのかをお話し、そこで得た学びを共有できればと思います!
API Gateway + Cognito
そもそもAPI Gatewayには2種類のAPIタイプがあり、 HTTP API
と REST API
が存在しています。
今回に必要な部分の違いを以下にまとめます。
HTTP API | REST API | |
---|---|---|
Cognito | ◯ (条件付き) | ◯ |
HTTP proxy | ◯ | ◯ |
Private integration | - | ◯ |
要件としてセキュアに接続したかったので、本番環境ではPrivate integrationが可能なREST APIを選択する必要がありました。しかし開発環境、ステージング環境ではセキュアである必要は特に無くコスト削減もあり、グローバルで構築する計画でした。
REST APIを選択し、HTTP Proxyを実現して内部のコンテナにアクセスさせる設計でしたが、ここで問題がありました。
VPCのプライベートDNSが有効になっている場合、API Gatewayのエンドポイントの解決されたDNSは、パブリックAPIのパブリックIPではなく、関連付けられたVPC エンドポイントのプライベートIPをポイントします。つまりグローバルからアクセスできなくなってしまいます。
既存のVPCではプライベートDNSを有効にしていました。無効にすると別の問題が発生してサービス影響があるかもしれないのと時間的な問題もあり、API Gateway+Cognitoの構成を断念しました。
ALB+Cognito
API Gatewayでは不可能だった内部アクセスをALBで代用する設計に変更しました。しかし、この構成でも問題点があり断念することになります。
以下の問題点が発生しました。
- React SPA問題
- セッション問題
- Privateアクセス問題
React SPA問題
ReactだとSPAのため、存在しないパスへのreloadなどのため、通常はWEBサーバーなどで404の場合は200を返すことを設定する必要があります。ですが、それをALBではできない点が問題になりました。サーバーサイドと相談し、パス方式(/aaa/bbb)ではなくハッシュ方式(/aaa#bbb)に変更してもらい対応しましたが、次の問題が発生しました。
セッション問題
ALB+Cognitoの際、認証のセッションは以下の通りになります。
セッション名 | 場所 | 説明 |
---|---|---|
SessionTimeout | ALB | アクセストークンの有効期限でCookieに使われる(最短1秒) |
access_token_validity | Cognito | アクセストークンの有効期限(最短15分) |
refresh_token_validity | Cognito | リフレッシュトークンの有効期限(最短1時間) |
要件として認証期間は短く設定したかったのと、ユーザー削除時は即座にログオフさせたいと考えておりました。
すべての有効期間を最短に設定した場合、実際の挙動は以下のようになります。
- ALBのSessionTimeoutの有効期間が切れた場合、Cognitoにアクセストークンを再取得する
- この際、
access_token_validity
が15分以内であれば、同じアクセストークンが発行される ※ 新たにアクセストークンが払い出されない access_token_validity
が15分以上の場合、新たにアクセストークンが発行される- しかし、
refresh_token_validity
が1時間以内はユーザーの存在確認を行わない(再認証が行われない)
つまり、ユーザーを削除しても最大1時間は再認証が行われず、ユーザーがログインできてしまう状態になります。
認証の流れは以下のような感じになります。
sequenceDiagram クライアント->>ALB: アクセス ALB-->>クライアント: SessionTimeoutの間は同じCookieを返す クライアント->>ALB: アクセス ALB->>Cognito: SessionTimeoutを超えた場合はアクセストークン更新要求 Cognito-->>ALB: access_token_validityの間は同じアクセストークンを返す Cognito-->>ALB: access_token_validityを超えた場合は新規トークンを返す ALB-->>クライアント: 更新されたCookieを返す クライアント->>ALB: アクセス ALB->>Cognito: SessionTimeoutを超えた場合はアクセストークン更新要求 Cognito->>Cognito_User: refresh_token_validityを超えた場合に再認証要求 Cognito_User-->>Cognito: 再認証されたアクセストークンを返す Cognito-->>ALB: 更新されたアクセストークンを返す ALB-->>クライアント: 更新されたCookieを返す
参考
ALB+Cognitoの場合に x-amzn-oidc-identity
ヘッダーをバックエンドに渡せるようにするには、ALBとCognitoのスコープにaws.cognito.signin.user.admin
を追加することでユーザーを識別できるヘッダーがALBにより付与されます。
Privateアクセス問題
ALBとCognitoをつなぎこんだ場合、内部的にALBからCognitoへアクセスが行われます。
Cognitoはグローバルなサービスとなっており、現状ではVPC EndpointのようなPrivateに接続するサービスは非対応のため、internalなALBの場合はCognitoへ通信することができません。
セキュアにするため、完全にPrivateで構築する必要があったため、ALB+Cognitoを断念することになります。
回避策
セキュリティレベルを1段階落とすことで回避策も可能でした。検討した内容をお話します。
- 該当subnetにNAT gatewayなどを設置し、外部通信のみ許可する
NAT gatewayを設置することでアウトバウンド通信は可能となり、internalなALBでもCognitoにアクセスすることが可能になります。
しかし、PCI DSS要件ではアウトバウンドのトラフィックも制御する必要があります。 そこで検討したのが AWS Network Firewall でした。
Cognitoへのアウトバウンドトラフィックを制御する際にNetwork ACLを使ってIPベースで制御しようとしました。
しかし、Cognitoで使用されるIPレンジは固定されておらず、ip-ranges.json でも service が EC2 となる IP アドレス範囲 すべてが対象とのことでNetwork ACLのテーブルが足りなくなります。
そこで、AWS Network Firewallではアウトバウンドのトラフィックをドメインレベルで指定して制御することが可能でした。
検証した結果うまく行ったのですが、コスト面を考えると見合っていないと判断し、断念しました。
まとめ
今回はセキュアなシステムを構築する際に試行錯誤した点や躓いた点を記述しました。
認証周りはCognitoを使って認証を実装しましたが、組み合わせる製品によっては挙動が思ったようにいかないケースが有りました。
セッションの問題は自分の理解が浅かったと反省するとともに、弊社のサーバーサイドと議論できたことが今回学びに繋がったと考えています。
読者の皆さんがセキュアなサービス作りを実施される際の参考になればと思っています。
スマートバンクでは一緒にサービスを作っていくSREチームのエンジニアを募集しています!