公開はするが、誰にも生では触らせない — セルフホストS3の多層防御という設計判断
はじめに
自宅NAS上のセルフホスト S3互換ストレージ(Garage)を外部公開するにあたって、「公開はするが、誰にも生のままでは触らせない」という方針で多層防御を組みました。
この記事は、手順やコードではなく なぜその積み方にしたのか、設計判断の理由を書きます。具体的な構築手順は別記事(インフラ編・アプリ実装編)にあります。
結論を先に言うと、守りは次の積層になりました。
- Cloudflare Access — 誰がそのホストに到達できるか(IPと、必要ならSSO)
- Garage の S3署名(SigV4) — 到達した先で、鍵を持つ者だけが操作できる
- 自前API + presigned URL — クライアントに生の鍵を渡さず、短命の署名URLだけを配る
それぞれが独立した関門で、1枚破られても次がある。そして重要なのは、各層が「何を守るか」を明確に分担していることです。
出発点の問い:公開と秘匿は両立するか
セルフホストのオブジェクトストレージを外から使いたい。でも素朴にポートを開けて公開すれば、それは攻撃対象です。かといって完全に閉じれば、外部のツールやクラウド上のアプリから使えない。
ここで陥りがちなのが「秘匿していれば安全」という発想です。URLを知られなければ大丈夫、というのは防御ではありません(security by obscurity)。実際、本構成の S3 エンドポイントは Cloudflare 配下でインターネットに出ています。隠していません。隠す代わりに、到達できても何もできないようにする方針を採りました。
「公開 vs 秘匿」の二択ではなく、「公開した上で、各層でクレデンシャルを要求する」。これが出発点です。
層1:Cloudflare Access — 「誰が到達できるか」
最前段は Cloudflare Access(Zero Trust)です。ここで「そもそもそのホスト名に到達できる相手」を絞ります。NAS のポートはルーターに一切開けず、Cloudflare Tunnel の outbound 接続だけで公開しているので、攻撃者が直接NASを叩く経路は存在しません。すべて Cloudflare のエッジを通ります。
Allow と Bypass — ここが一番の設計判断
Cloudflare Access のポリシーには Allow と Bypass があり、この使い分けが本構成の肝でした。
- Allow = 条件(IP等)に合う相手を通すが、ログイン(SSO)を要求する
- Bypass = 条件(IP/CIDR)に合えば、ログイン無しで素通りさせる
そして決定的なのは、守る対象が「アプリ自身の認証を持つか持たないか」で、どちらを使うべきかが変わるという点です。
S3 API → Bypass
S3 API のホストは、Garage 自身が S3署名(SigV4)という認証機構を持っています。つまり認証は Garage に任せられる。ここで Access に SSO ログインまで要求させる(Allow)と、curl や自動化ツール、presigned URL での直アクセスがログイン画面に弾かれてしまいます。
なので S3 側は 許可IPを Bypass(素通り) にして、認証は後段の Garage 署名に委ねる。Access は「許可IPかどうか」だけを見る門番に徹します。
これは実際にハマったポイントでもあります。当初 Allow にしていて、IPが一致しているのに presigned URL が 302 でSSOログインに飛ばされ続けました。Bypass に変えて初めて、署名付きURLが素通り→Garage の署名ゲートに到達、という意図どおりの挙動になりました。「Access の 302 が消えて、Garage の 403 が返る」が、正しく到達した合図です。
管理UI → Allow + SSO
一方、Garage の管理UI(サードパーティの garage-webui)には、アプリ自体にログイン認証がありません。素のまま公開すると、URLを知る者は誰でもバケットやキーを操作できてしまう。
ここでは認証を Cloudflare Access に肩代わりさせます。許可IP「かつ」特定ユーザーのSSOログイン(Allow + ユーザー固定)という二重の門。管理UIは鍵そのものを発行・閲覧・削除できる最も危険な画面なので、S3 API より一段厳しくするのが筋です。
対比:Access の役割は固定ではない
| S3 API | 管理UI | |
|---|---|---|
| アプリ自身の認証 | あり(S3署名) | なし |
| Access の役割 | 門番(IPを通すだけ) | 認証本体 |
| ポリシー | Bypass | Allow + SSO |
同じ Cloudflare Access でも、対象のアプリが認証を持つかどうかで役割が真逆になる。 認証を持つアプリには「素通りの門番」、持たないアプリには「認証そのもの」。これが今回いちばん腑に落ちた設計判断でした。認証のないセルフホストアプリを、ソースに一切手を入れず安全に公開できる、という汎用パターンでもあります。
層2:Garage の S3署名 — 「鍵を持つ者だけが操作できる」
Access を通った先で、Garage 自身の認証が効きます。バケットは private、匿名アクセスは拒否(403)。SigV4 署名のない素のリクエストは、たとえ許可IPから来ても弾かれます。
層1(Access)が「誰が来たか」を、層2(Garage署名)が「正しい鍵を持っているか」を見る。到達制御と操作制御を別の層に分けているわけです。許可IPの内側に万一信頼できない端末が紛れても、鍵がなければ何もできない。
層3:自前API + presigned URL — 「生の鍵を配らない」
最上層は、アプリ側の判断です。クライアントに access/secret key を直接渡すと、漏れたら全権限が漏れます。そこで、
- 鍵はサーバ側(自前API)だけが保持する
- クライアントはAPIに Bearer トークンで認証し、ファイルを要求する
- APIは presigned URL(短命・例:10分) を生成して返す
- クライアントはその署名URLで Garage へアクセスする
こうすると、クライアントに出ていくのは「10分で失効する、その1ファイル限定の署名URL」だけ。漏れても被害は限定的で短命です。生の鍵は一度もネットワークに出ません。
ただしこの「被害限定」は、クライアントに渡すものが presigned URL だけである限り、という前提つきです。Bearer トークン・サーバ側が保持する access/secret key・Cloudflare の設定そのものが漏れれば、それは別次元の話で、限定では済みません。守るべき秘密の優先順位を取り違えないことが大事です。
さらに、人間が使う認証済みUIに対しては presigned URL すら露出させず、APIサーバが中継して配信する経路も用意しました。自動化ツールには軽量な直アクセス(presigned)、人間には漏れない中継、と使い分けています(詳細はアプリ実装編)。
なぜ層を分けるのか — 単一障害点を作らない
一枚岩の防御は、その一枚が破られたら終わりです。多層にする狙いは、各層が独立して、別の観点で守ることにあります。
- Access(IP/SSO)が誤設定で緩んでも → Garage署名が匿名を拒否する
- Garage の鍵が漏れても → presigned 方式なら生キーは出回っていない(漏れるのは短命URL)
- presigned URL が漏れても → 10分で失効し、許可IP外からは層1で弾かれる
どの層も「これさえあれば全部通る」という万能の穴を持たない。これが「公開はするが、誰にも生では触らせない」の実体です。
留意点 — 多層防御は運用で崩れる
層を増やすほど、設定ミスや運用のズレで崩れる箇所も増えます。実際に踏んだ/気をつけた点を挙げます。
- Allow/Bypass の取り違え: S3 を Allow にすると自動化が止まる。管理UIを Bypass にすると認証が消える。役割を間違えると、片や使えず、片や無防備になる。
- 許可IPの前提: IP許可方式は固定IPが前提。動的IPだと再接続でズレて、正規の相手が弾かれる(あるいは穴が開く)。固定IPか、サービストークン等のIP非依存な手段に。
- 管理ポートを晒さない: 公開するのはS3 APIと管理UIのホスト名だけ。RPC や admin API のポートは外に出さない。
- presigned はログ・チャットに残さない: 短命でも鍵。共有経路に貼れば失効までは流出経路になる。
多層防御は「組んで終わり」ではなく、各層が意図どおり効いているかを確認し続けて初めて成立します。
まとめ
「公開 vs 秘匿」ではなく「公開した上で、各層で別々のクレデンシャルを要求する」。これがセルフホストS3を外部公開するときに採った方針でした。
要は、
- 層1(Cloudflare Access)が 到達を制御し、アプリの認証有無で門番にも認証本体にもなる
- 層2(Garage署名)が 操作を制御し、鍵を持つ者だけに許す
- 層3(自前API + presigned)が 鍵の配り方を制御し、生キーを一度も外に出さない
それぞれが独立に守るから、一枚破られても次がある。手順そのものより、この「どの層に何を守らせるか」の分担が、再利用できる設計だと思っています。
(構築手順はインフラ編・アプリ実装編に分けてあります。)