HAProxyによるWasabiのPublic Accessプロキシ実装(for Mastodon)

Mastodonのオブジェクトストレージとして利用してみるやつ。

Wasabiは2023年春ごろからPublic accessが原則使えなくなっています。詳細は以下参照。

Wasabiのパブリックアクセスが使えなくなった件(対応の仕方)

上記の@noellaboさんの記事では、nginxとlua(openresty)を用いた対応例が示されていますが、今回は別のアプローチとして表題の通りHAProxyを用いたゲートウェイを作ります。

https://github.com/jouve/haproxy-s3-gateway/blob/main/haproxy/haproxy.cfg

上記の「s3v4」と「upstream」をほぼそのまま利用します。set-var-fmtがHAProxy 2.5以降の実装であるため、標準で新し目のバージョンが入らないOSをお使いの人は適宜対応してください。

万が一アクセスキーなどが漏れても参照しかできないように、必要最低限のキーをWasabiで作成してそちらを利用します。

また、WasabiのEndpointのIPが変わっても動作するよう、resolverの設定を追加します。HAProxyはresolver設定を行わないと、プロセス起動時に名前解決を行ったきりそれ以降DNSへの問い合わせを行わない、という挙動を示します。(2.0等といった昔の版は確実に。最近のはちゃんと調べていないです。)

今回、Proxyを動作させるOSは既存弊Mastodonサーバと合わせてUbuntuとしたので、以下を参考にppaを追加します。
https://haproxy.debian.net/

また、上記の設定例だとアットマークを含む画像(サーバのaboutページなどに使われている)がうまく表示されないため、以下のようなFrontendを追加し、そちらを443待ち受けで使用するように変更。

また、backendも追加し、redispatchオプション+retry-onオプションによりオブジェクトストレージ側で404やリトライ可能エラーが出た場合に旧サーバ(非オブジェクトストレージ)の参照やエラー画像を返すサーバを指定しています。(旧サーバについては、ファイル移行中のフォールバック目的です。ファイル移動が終わったら削除可能です。 ※2024/2/16追記記事を公開したのでリンク)

frontend fr-1
bind :::443 v4v6 ssl crt <pemファイルなど> alpn h2,http/1.1
http-request set-path %[path,regsub(@,%40,g)]
http-request set-var(txn.path) path
acl ismedia var(txn.path) -i -m end png
acl ismedia var(txn.path) -i -m end jpg
acl ismedia var(txn.path) -i -m end jpeg
acl ismedia var(txn.path) -i -m end bmp
acl ismedia var(txn.path) -i -m end gif
acl ismedia var(txn.path) -i -m end jfif
acl ismedia var(txn.path) -i -m end webp
acl ismedia var(txn.path) -i -m end mp4
acl ismedia var(txn.path) -i -m end avi
acl ismedia var(txn.path) -i -m end mpg
acl ismedia var(txn.path) -i -m end mov
acl ismedia var(txn.path) -i -m end mkv
acl ismedia var(txn.path) -i -m end flv
acl ismedia var(txn.path) -i -m end webm
acl ismedia var(txn.path) -i -m end m4v

http-response del-header X-Amz-Id-2
http-response del-header X-Amz-Request-Id
http-response del-header X-Wasabi-Cm-Reference-Id

http-response add-header Access-Control-Allow-Origin * if ismedia
http-response add-header X-Nsd-IsMedia Yes if ismedia
http-response add-header X-Nsd-IsMedia No if !ismedia

default_backend bk-1

backend bk-1
balance first
option redispatch
retry-on 404 all-retryable-errors
server listen-wasabi localhost:8081 check
server listen-origin-cache localhost:8082 check
server listen-error-bk localhost:8083 check

Mastodon側では以下のように.env.production(など)へWasabiのEndpointや書き込み可能なAccessKey、参照用FQDNの指定などを行います。

S3_ENABLED=true
S3_BUCKET=<バケット名>
AWS_ACCESS_KEY_ID=<アクセスキー>
AWS_SECRET_ACCESS_KEY=<Secret>
S3_ENDPOINT=https://s3.ap-northeast-1.wasabisys.com
S3_ALIAS_HOST=obj1.oyasumi.dev

うまく行けば上記のように、Wasabi上の画像が取得できるはずです。

参照用FQDNについては、Cloudflareのキャッシュ機能の対象にするなどして、回線に負荷がかからないようにするとより良いでしょう。

最後に、用いている設定を以下に示します。(参考)
尚、実運用ではもう少しダーティなことを行っています。※改善点とかあればこっそり教えて下さい(^o^)

global
log /dev/log local0
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon

# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private

# generated 2024-01-15, Mozilla Guideline v5.7, HAProxy 2.8.5, OpenSSL 1.1.1w, intermediate configuration, no HSTS, no OCSP
# https://ssl-config.mozilla.org/#server=haproxy&version=2.8.5&config=intermediate&openssl=1.1.1w&hsts=false&ocsp=false&guideline=5.7
# intermediate configuration
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets

ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets

# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/haproxy/ffdhe2048.txt
ssl-dh-param-file /etc/haproxy/ffdhe2048.txt

defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http

frontend fr-1
bind :::443 v4v6 ssl crt <pemファイルなど> alpn h2,http/1.1
http-request set-path %[path,regsub(@,%40,g)]
http-request set-var(txn.path) path
acl ismedia var(txn.path) -i -m end png
acl ismedia var(txn.path) -i -m end jpg
acl ismedia var(txn.path) -i -m end jpeg
acl ismedia var(txn.path) -i -m end bmp
acl ismedia var(txn.path) -i -m end gif
acl ismedia var(txn.path) -i -m end jfif
acl ismedia var(txn.path) -i -m end webp
acl ismedia var(txn.path) -i -m end mp4
acl ismedia var(txn.path) -i -m end avi
acl ismedia var(txn.path) -i -m end mpg
acl ismedia var(txn.path) -i -m end mov
acl ismedia var(txn.path) -i -m end mkv
acl ismedia var(txn.path) -i -m end flv
acl ismedia var(txn.path) -i -m end webm
acl ismedia var(txn.path) -i -m end m4v

http-response del-header X-Amz-Id-2
http-response del-header X-Amz-Request-Id
http-response del-header X-Wasabi-Cm-Reference-Id

http-response add-header Access-Control-Allow-Origin * if ismedia
http-response add-header X-Nsd-IsMedia Yes if ismedia
http-response add-header X-Nsd-IsMedia No if !ismedia

default_backend bk-1

backend bk-1
balance first
option redispatch
retry-on 404 all-retryable-errors
server listen-wasabi localhost:8081 check
server listen-origin-media localhost:8082 check
server listen-error-srv localhost:8083 check

listen s3v4
bind :::8081 v4v6
http-request set-var(req.region) str(ap-northeast-1)
http-request set-var(req.endpoint) str(s3.ap-northeast-1.wasabisys.com:443)
http-request set-var(req.bucket) str(<ばけっと>)
http-request set-var(req.access_key) str(<あくせすきー>)
http-request set-var(req.secret_key) str(<Secret>)

http-request set-path /%[var(req.bucket)]%[path]

http-request set-var(req.date) date
http-request set-var(req.x_amz_date) var(req.date),utime("%Y%m%dT%H%M%SZ")
http-request set-var(req.today) var(req.date),utime("%Y%m%d")

http-request set-header Host %[var(req.endpoint)]
http-request set-header x-amz-date %[var(req.x_amz_date)]
http-request set-var(req.x_amz_content_sha256) str(e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855) if { req.body_size eq 0 }
http-request set-var(req.x_amz_content_sha256) var(req.body),sha2(256),hex,lower if { req.body_size gt 0 }
http-request set-header x-amz-content-sha256 %[var(req.x_amz_content_sha256)]
http-request set-var-fmt(req.scope) %[var(req.today)]/%[var(req.region)]/s3/aws4_request
http-request set-var-fmt(req.canonical_request) %[method]\n%[path]\n\nhost:%[var(req.endpoint)]\nx-amz-content-sha256:%[var(req.x_amz_content_sha256)]\nx-amz-date:%[var(req.x_amz_date)]\n\nhost;x-amz-content-sha256;x-amz-date\n%[var(req.x_amz_content_sha256)]
http-request set-var-fmt(req.string_to_sign) AWS4-HMAC-SHA256\n%[var(req.x_amz_date)]\n%[var(req.scope)]\n%[var(req.canonical_request),sha2(256),hex,lower]
http-request set-var(req.key) str(),concat(AWS4,req.secret_key),base64
http-request set-var(req.date_key) var(req.today),hmac("sha256",req.key),base64
http-request set-var(req.date_region_key) var(req.region),hmac("sha256",req.date_key),base64
http-request set-var(req.date_region_service_key) str(s3),hmac("sha256",req.date_region_key),base64
http-request set-var(req.signing_key) str(aws4_request),hmac("sha256",req.date_region_service_key),base64
http-request set-var(req.signature) var(req.string_to_sign),hmac("sha256",req.signing_key),hex,lower

http-request set-header Authorization "AWS4-HMAC-SHA256 Credential=%[var(req.access_key)]/%[var(req.scope)],SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=%[var(req.signature)]"

server local localhost:8084 check

listen upstream
bind :::8084 v4v6
capture request header Authorization len 1000
capture request header Date len 1000
capture request header X-Amz-Content-Sha256 len 1000
capture request header X-Amz-Date len 1000
server wasabi-upstream s3.ap-northeast-1.wasabisys.com:443 ssl verify none resolvers dns-resolver alpn h2,http/1.1 check

resolvers dns-resolver
nameserver ns1 1.1.1.1:53
nameserver ns2 8.8.8.8:53

listen origin-media
bind :::8082 v4v6
http-request set-header Client-Source-IP %[req.hdr_ip(X-Forwarded-For,1)]
http-request set-header X-Forwarded-Proto https
http-request set-header host <丼さーば>
http-request set-path /system%[path]
server mastodon-direct <丼さーば>:443 check ssl verify none resolvers dns-resolver alpn h2,http/1.1

listen error-srv
bind :::8083 v4v6
http-request set-path /img/404.png if { path -i -m end png }
http-request set-path /img/404.jpg if { path -i -m end jpg }
http-request set-path /img/404.gif if { path -i -m end gif }
http-request set-path /img/404.jpg if { path -i -m end jfif }
http-request set-path /img/404.jpg if { path -i -m end jpeg }
http-request set-path /img/404.webp if { path -i -m end webp }
http-response set-status 503
server http-server <エラー応答用サーバ>:80 check

「HAProxyによるWasabiのPublic Accessプロキシ実装(for Mastodon)」への1件のフィードバック

コメントする

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください