Amazon SNS で Basic 認証の HTTPS エンドポイントサブスクリプションを試してみた

2023.01.13

いわさです。

メッセージングサービスの Amazon SNS を使うと様々な送信先にメッセージを送信することが出来ます。
サポートされている送信先のひとつに HTTP/HTTPS エンドポイントがあります。

Basic 認証を設定したエンドポイントに対してメッセージを送信する機会があったので試したことなどをご紹介します。

API Gateway + Lambda オーソライザーで Basic 認証を設定したエンドポイントを用意

この記事では以下を参考に送信先のエンドポイントを用意しました。
Lambda オーソライザーとカスタムゲートウェイレスポンスで Basic 認証を実現していた API Gateway です。

上記記事と違ってバックエンドは Mock を選択しています。
今回は SNS から Basic 認証が利用出来るのかの実証とリクエスト内容の一部を除くことを目的にしているため、以下のように CloudWatch ログ設定を行いリクエスト内容を観察してみたいと思います。

ステージデプロイ後にブラウザでアクセスしてみると以下のように Basic 認証が有効になっていることが確認出来ます。

正しい認証情報を入力した際に、API Gateway 側では以下のようなログが確認出来ます。
リクエストペイロードも確認出来るのですが、Authorization ヘッダーなどの秘匿情報はログ上はマスクされる仕様になっています。

ここでは API Gateway オブジェクトへ Transform する部分のログを確認しました。
以下のハイライト部分で Base64 エンコードされた認証情報が確認出来ました。

ブラウザから

{
    "type": "REQUEST",
    "methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:ppbmxpwne5/hoge-stage/GET/",
    "resource": "/",
    "path": "/",
    "httpMethod": "ANY",
    "headers": {
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "accept-encoding": "gzip, deflate, br",
        "accept-language": "en-US,en;q=0.9",
        "authorization": "Basic YWRtaW46cGFzc3dvcmQ=",
        "cache-control": "max-age=0",
        "Host": "ppbmxpwne5.execute-api.ap-northeast-1.amazonaws.com",
        "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Google Chrome\";v=\"108\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"macOS\"",
        "sec-fetch-dest": "document",
        "sec-fetch-mode": "navigate",
        "sec-fetch-site": "none",
        "sec-fetch-user": "?1",
        "upgrade-insecure-requests": "1",
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
        "X-Amzn-Trace-Id": "Root=1-63c0c6fe-5881ea6b5c79"
:
    }
}

SNS トピックにサブスクリプションを追加

Amazon SNS にて適当なスタンダードトピックを作成します。

HTTPS プロトコルのサブスクリプションを作成します。
後ほど検証結果やエラーメッセージも紹介しますが、HTTP プロトコルでは Basic 認証を使うことが出来ません。HTTPS 必須です。

形式としては公式ドキュメントに記載のとおり https://[ユーザー名]:[パスワード]@FQDN です。

パスワードは基本マスクされるが、マスクされない API もある

なお、入力したパスワードはマネジメントコンソール上は以下のようにマスクされて表示されます。

ちなみに AWS CLI だとsns list-subscriptions系のコマンドだとマスクされますが、sns get-subscription-attributesだとマスクされませんでした。なんてこったい。
こういう挙動になるという点を認識しポリシーは慎重に決めたほうが良さそうですね。

% aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns                                            
{
    "Subscriptions": [
        {
            "SubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns:523ddde6-0391-40d5-88ca-56c1ff1d8f76",
            "Owner": "123456789012",
            "Protocol": "https",
            "Endpoint": "https://admin:****@ppbmxpwne5.execute-api.ap-northeast-1.amazonaws.com/hoge-stage",
            "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns"
        }
    ]
}

% aws sns get-subscription-attributes --subscription-arn arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns:523ddde6-0391-40d5-88ca-56c1ff1d8f76
{
    "Attributes": {
        "SubscriptionPrincipal": "arn:aws:iam::123456789012:role/cm-iwasa.takahito",
        "Owner": "123456789012",
        "RawMessageDelivery": "false",
        "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns",
        "Endpoint": "https://admin:password@ppbmxpwne5.execute-api.ap-northeast-1.amazonaws.com/hoge-stage",
        "EffectiveDeliveryPolicy": "{\"healthyRetryPolicy\":{\"minDelayTarget\":20,\"maxDelayTarget\":20,\"numRetries\":3,\"numMaxDelayRetries\":0,\"numNoDelayRetries\":0,\"numMinDelayRetries\":0,\"backoffFunction\":\"linear\"},\"sicklyRetryPolicy\":null,\"throttlePolicy\":null,\"guaranteed\":false}",
        "Protocol": "https",
        "PendingConfirmation": "false",
        "ConfirmationWasAuthenticated": "false",
        "SubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns:523ddde6-0391-40d5-88ca-56c1ff1d8f76"
    }
}

Authorization ヘッダーが先頭大文字になる

サブスクリプションを作成すると、エンドポイントに許可 URL を含めた確認メッセージが送信されます、が送信されませんでした。
API Gateway のログを見てみると認証エラーになっているので Basic 認証を通過出来ていないようです。

先程と同様にどういうリクエストが送信されているのかを API Gateway のログから確認してみます。

SNSから

{
    "type": "REQUEST",
    "methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:ppbmxpwne5/hoge-stage/POST/",
    "resource": "/",
    "path": "/",
    "httpMethod": "ANY",
    "headers": {
        "Accept-Encoding": "gzip,deflate",
        "Authorization": "Basic YWRtaW46cGFzc3dvcmQ=",
        "Content-Length": "1609",
        "Content-Type": "text/plain; charset=UTF-8",
        "Host": "ppbmxpwne5.execute-api.ap-northeast-1.amazonaws.com",
        "User-Agent": "Amazon Simple Notification Service Agent",
        "x-amz-sns-message-id": "aa61d4ae-50f5-4875-807d-7e4fcadd3ea8",
        "x-amz-sns-message-type": "SubscriptionConfirmation",
        "x-amz-sns-topic-arn": "arn:aws:sns:ap-northeast-1:123456789012:hoge0113sns",
        "X-Amzn-Trace-Id": "Root=1-63c0c80b-2b3a82520a7053064be7bfe7",
        "X-Forwarded-For": "54.240.200.68",
        "X-Forwarded-Port": "443",
        "X-Forwarded-Proto": "https"
    },
    "multiValueHeaders": {
        "Accept-Encoding": [
            "gzip,deflate"
        ],
        "Authorization": [
            "Basic YWRtaW46cGFzc3dvcmQ="
        ],
        "Content-Length": [
            "1609"
        ],
        "Content-Type": [
            "text/plain; charset=UTF-8"
        ]
:
    }
}

Authorization ヘッダーにユーザー名とパスワードをコロンで結合して Base64 エンコードされた情報が設定されますが、そのヘッダーキーが大文字始まりになっていますね。
Lambda オーソライザーの関数では全て小文字を想定しているのでここでエラーになっていたようです。

今回は単純な検証が目的なので以下のように大文字開始を想定するロジックに変更しました。
実際のところ小文字も処理出来る必要がありますし、HTTP2 のヘッダー仕様についても認識しておく必要があります。が、今回は Lambda オーソライザーの実装方法は本題ではないのでこれで検証を進めます。

index.js

exports.handler = function (event, context, callback) {
  //var authorizationHeader = event.headers.authorization
  var authorizationHeader = event.headers['Authorization'];

  if (!authorizationHeader) return callback('Unauthorized')

  var encodedCreds = authorizationHeader.split(" ")[1]
  var plainCreds = (new Buffer(encodedCreds, 'base64')).toString().split(':')
:

メッセージを送信してみる

再度サブスクリプションを作成してみると、Lambda オーソライザーが生成した独自のプリンシパルがログで確認出来て、サブスクリプションの検証も実施出来ると思います。

では、最後にメッセージを送信してみます。

ログを確認してみると、以下のように Lambda オーソライザーで許可されていることと、API Gateway のバックエンド(ここでは Mock)から期待したレスポンスが取得出来ていることが確認出来ました。

HTTP プロトコルでは Basic 認証は使えない

ドキュメントには HTTPS エンドポイントと明記されていましたが、HTTP エンドポイントだとどうなるのでしょうか。
試してみました。

入力自体は出来ますが、エラーメッセージが表示されサブスクリプション作成に失敗しました。なるほど!

Invalid parameter: Endpoint Reason: Please use HTTPS for subscriptions that contain inline credentials

さいごに

本日は Amazon SNS で Basic 認証の HTTPS エンドポイントサブスクリプションを試してみました。
Basic 認証が最初うまく通らず困っていましたが、リクエストをよくよく見てみるとヘッダー名が原因でした。うまく Basic 認証通らないという方はリクエスト情報を観察してみてください。