Apache HTTP ServerでALBのヘルスチェックのみBasic認証を回避するよう設定してみた

複雑な条件でリスナールール数が非常に多くなるようであればバックエンド側でBasic認証の設定をするのが良さそう
2023.10.28

ALBからのヘルスチェック かつ ヘルスチェックパス宛の通信のみBasic認証を回避させたい

こんにちは、のんピ(@non____97)です。

皆さんはALBからのヘルスチェック かつ ヘルスチェックパス宛の通信のみBasic認証を回避させたいと思ったことはありますか? 私はあります。

「限られたメンバーにしか通信できないようにしたいが、送信元IPアドレスでは絞りにくい」といったときはBasic認証を使うことがあると思います。

Basic認証を回避できなければALBのヘルスチェックは401エラーとなってしまいます。

既に「ヘルスチェックパスの宛の通信は許可する」というアプローチでBasic認証を回避する記事は存在しています。

こちらの記事が投稿されてから10年以上経っており、Order/Allow/Denyディレクティブを使っていたりとしているので、Apache 2.4的な定義の仕方で実装してみます。

Apache 2.4系でOrder/Allow/Denyディレクティブを使うとAH01797: client denied by server configuration:というメッセージがエラーログに出力され、目障りなのでなんとかしたいところです。

なお、「大量の送信元IPアドレス、ユーザーエージェント、リクエストパスの組み合わせでBasic認証をしたい」という訳でなければ、Lambda関数でBasic認証をかける方が楽です。

いきなりまとめ

  • Requireディレクティブを使おう
  • 「全ての条件を満たす」を定義したい場合はRequireAnyディレクティブを使おう

検証環境

検証環境は以下のとおりです。

Apache HTTP ServerでALBのヘルスチェックのみBasic認証を回避するよう設定してみた検証環境構成図

WebサーバーとしてApache HTTP Serverをインストールしました。また、なんとなくApache Tomcatもセットアップしました。

環境はAWS CDKでデプロイしました。使用したコードは以下リポジトリに保存しています。

UserDataAssetを組み合わせると、簡単に大量の設定ファイルをEC2インスタンスにアップロードすることが出来て非常に便利です。

./lib/constructs/autoscaling-group.ts

    // Assets
    const httpdAssets = new cdk.aws_s3_assets.Asset(this, "HttpdAssets", {
      path: path.join(__dirname, "../ec2/httpd"),
    });

    const tomcatAssets = new cdk.aws_s3_assets.Asset(this, "TomcatAssets", {
      path: path.join(__dirname, "../ec2/tomcat"),
    });

    // IAM Role
    const role = new cdk.aws_iam.Role(this, "Role", {
      assumedBy: new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        new cdk.aws_iam.ManagedPolicy(this, "Policy", {
          statements: [
            new cdk.aws_iam.PolicyStatement({
              effect: cdk.aws_iam.Effect.ALLOW,
              resources: [
                `arn:aws:s3:::${httpdAssets.s3BucketName}/*`,
                `arn:aws:s3:::${tomcatAssets.s3BucketName}/*`,
              ],
              actions: ["s3:GetObject"],
            }),
          ],
        }),
      ],
    });

    // User data
    const userData = cdk.aws_ec2.UserData.forLinux();
    userData.addCommands(
      fs.readFileSync(
        path.join(__dirname, "../ec2/user-data/default.sh"),
        "utf8"
      )
    );
    userData.addS3DownloadCommand({
      bucket: httpdAssets.bucket,
      bucketKey: httpdAssets.s3ObjectKey,
      localFile: "/tmp/assets/httpd.zip",
    });
    userData.addS3DownloadCommand({
      bucket: tomcatAssets.bucket,
      bucketKey: tomcatAssets.s3ObjectKey,
      localFile: "/tmp/assets/tomcat.zip",
    });
    userData.addCommands(
      fs.readFileSync(
        path.join(__dirname, "../ec2/user-data/setting-packages.sh"),
        "utf8"
      )
    );

肝心のApache HTTP Serverの設定は以下のとおりです。

./lib/ec2/httpd/conf.d/httpd-vhosts.conf

<VirtualHost *:80>
    ServerName sample1.web.non-97.net
    DocumentRoot /var/www/html/sample1

    ProxyPass /tomcat ajp://localhost:8009/ secret=AjpSecret5
    ProxyPassReverse /tomcat ajp://localhost:8009/ secret=AjpSecret5

    ErrorLog /var/log/httpd/sample1_error_log
    CustomLog /var/log/httpd/sample1_access_log combined

    Timeout 120
    KeepAlive On
    KeepAliveTimeout 120

    <Directory /var/www/html/sample1>
        Options FollowSymLinks
        AllowOverride All
        Require all denied
    </Directory>

    <Location / >
        SetEnvIf User-Agent "^ELB-HealthChecker.*$" elbHealthCheckUserAgent
        SetEnvIf Request_URI "^/tomcat$" elbHealthCheckUri

        <RequireAll>
            Require env elbHealthCheckUserAgent
            Require env elbHealthCheckUri
        </RequireAll>

        <RequireAny>
            AuthType Basic
            AuthName "Input your ID and Password."
            AuthGroupFile /dev/null
            AuthUserFile "/etc/httpd/conf.d/.htpasswd"
            Require valid-user
        </RequireAny>
    </Location>
</VirtualHost>

ドキュメントルート/var/www/html/sample1のDirectoryディレクティブで、Require all deniedで拒否するようにしています。

Locationディレクティブの/にて以下のand条件に当てはまらない場合はBasic認証をするように定義をしています。

  • リクエストパスがALBのヘルスチェックパスである/tomcat
  • ユーザーエージェントがALBのヘルスチェックのものであるELB-HealthChecker

SetEnvIf Request_URI /tomcat elbHealthCheckUriだと、/tomcats/tomcat/pathなど前方一致する場合もマッチしてしまうので、完全一致するように明示します。

SetEnvIfRequire envを組み合わせれば大概のものは定義できそうな気がします。Requireディレクティブはmod_authz_coreモジュールをインポートすると使えます。

動作確認

AWS CDKでリソースをデプロイした後、動作確認をします。

アクセスログを確認すると、以下のようにALBのヘルスチェックの通信が記録されていました。

$ less /var/log/httpd/sample1_access_log
10.10.10.14 - - - [28/Oct/2023:07:56:19 +0000] "GET /tomcat HTTP/1.1" 200 65 2630 "-" "ELB-HealthChecker/2.0"
10.10.10.57 - - - [28/Oct/2023:07:56:19 +0000] "GET /tomcat HTTP/1.1" 200 65 1974 "-" "ELB-HealthChecker/2.0"
10.10.10.14 - - - [28/Oct/2023:07:56:29 +0000] "GET /tomcat HTTP/1.1" 200 65 1633 "-" "ELB-HealthChecker/2.0"
10.10.10.57 - - - [28/Oct/2023:07:56:29 +0000] "GET /tomcat HTTP/1.1" 200 65 2283 "-" "ELB-HealthChecker/2.0"

実際にアクセスして、Basic認証を回避できるパターンを確認しましょう。

# ユーザーエージェントを指定しない かつ ALBのヘルスチェックパス以外
$ curl -I https://sample1.web.non-97.net
HTTP/2 401
date: Sat, 28 Oct 2023 11:56:18 GMT
content-type: text/html; charset=iso-8859-1
server: Apache
www-authenticate: Basic realm="Input your ID and Password."

# ユーザーエージェントを指定しない かつ ALBのヘルスチェックパス
$ curl -I https://sample1.web.non-97.net/tomcat
HTTP/2 401
date: Sat, 28 Oct 2023 11:56:24 GMT
content-type: text/html; charset=iso-8859-1
server: Apache
www-authenticate: Basic realm="Input your ID and Password."

# ユーザーエージェントを指定 かつ ALBのヘルスチェックパス以外
$ curl -I https://sample1.web.non-97.net/ -A "ELB-HealthChecker/2.0"
HTTP/2 401
date: Sat, 28 Oct 2023 11:56:32 GMT
content-type: text/html; charset=iso-8859-1
server: Apache
www-authenticate: Basic realm="Input your ID and Password."

# ユーザーエージェントを指定 かつ ALBのヘルスチェックパス
$ curl -I https://sample1.web.non-97.net/tomcat -A "ELB-HealthChecker/2.0"
HTTP/2 200
date: Sat, 28 Oct 2023 11:56:38 GMT
content-type: text/html;charset=UTF-8
content-length: 65
server: Apache
set-cookie: JSESSIONID=0053D0857967DD0A5E9F719206A02733; Path=/; HttpOnly
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
x-xss-protection: 1; mode=block

# ユーザーエージェントを指定 かつ ALBのヘルスチェックパスの前方一致
$ curl -I https://sample1.web.non-97.net/tomcats -A "ELB-HealthChecker/2.0"
HTTP/2 401
date: Sat, 28 Oct 2023 11:57:03 GMT
content-type: text/html; charset=iso-8859-1
server: Apache
www-authenticate: Basic realm="Input your ID and Password."

ALBからのヘルスチェック かつ ヘルスチェックパス宛の通信のみBasic認証を回避できていますね。

もちろん、条件に当てはまらない場合でもBasic認証の認証情報を渡してあげれば、ステータスコード200が返ってきます。

$ curl -I https://sample1.web.non-97.net/ -u test-user:test-p@ssw0rd
HTTP/2 200
date: Sat, 28 Oct 2023 11:56:50 GMT
content-type: text/html; charset=UTF-8
content-length: 36
server: Apache
last-modified: Sat, 28 Oct 2023 11:23:57 GMT
etag: "24-608c50a7308a1"
accept-ranges: bytes
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
x-xss-protection: 1; mode=block

複雑な条件でリスナールール数が非常に多くなるようであればバックエンド側でBasic認証の設定をするのが良さそう

Apache HTTP ServerでALBのヘルスチェックのみBasic認証を回避するよう設定してみました。

Basic認証を回避させる条件がシンプルであれば、ALBのリスナールールとLambda関数を組みわせるパターンが良いでしょう。

ただし、条件が複雑でルール数が非常に多くなるようであればバックエンド側でBasic認証の設定をするのが良さそうです。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!