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ディレクティブを使おう
検証環境
検証環境は以下のとおりです。
WebサーバーとしてApache HTTP Serverをインストールしました。また、なんとなくApache Tomcatもセットアップしました。
環境はAWS CDKでデプロイしました。使用したコードは以下リポジトリに保存しています。
UserDataとAssetを組み合わせると、簡単に大量の設定ファイルを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
など前方一致する場合もマッチしてしまうので、完全一致するように明示します。
SetEnvIf
とRequire 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)でした!