0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIからも人間からもアクセスできるNASなS3をさくらレンサバにつなげた話 #1

0
Last updated at Posted at 2026-06-02

認証のないセルフホストS3を、コードに手を入れず安全に公開する — DS918+ × Garage × Cloudflare Tunnel × Access

はじめに

Synology DS918+ に S3互換オブジェクトストレージ Garage を Docker Compose で立て、Cloudflare Tunnel(cloudflared) で外部公開し、Cloudflare Access でアクセス制御した構成をまとめます。コピペで再現できることを目指しました。

この記事の肝は、最後の「守り方」です。

  • 自宅 NAS のポートをルーターに一切開けない(Tunnel の outbound 接続だけで公開)
  • S3 API と 管理UI を、同じトンネルから別ホスト名で出し分ける
  • そして両者で Cloudflare Access の役割を意図的に変える:
    • S3 API は Garage 自身が S3署名(SigV4)認証を持つ → Access は許可IPを**素通り(Bypass)**させ、認証は Garage に任せる
    • 管理UI(garage-webui)アプリ自体に認証が無い → Access が認証本体になり、許可IP かつ SSO ログイン(Allow) の二重で締める

特に管理UIのように 認証機構を持たないセルフホストアプリを、ソースに手を入れず Zero Trust で外付け認証するのは、Cloudflare Access の最も教科書的な使い方です。一方 S3 API 側は、Garage 自身が署名認証を持つので、Access は「到達元IPを絞る門番」に徹します(詳細は後述)。

なぜ MinIO ではなく Garage か

定番の MinIO ではなく Garage を選んだのには、軽さ以上の理由があります。

2025年5月のリリース(2025-05-24 系)以降、MinIO はコミュニティ版の Web コンソールから管理機能を整理しました。 アカウント・ポリシー管理、設定、バケット管理などがブラウザUIから外れ、コンソールは実質「オブジェクトブラウザ」に近い形になり、これらの操作は mc(コマンドラインクライアント)で行う流れになりました。あわせて、コンソールの外部IDPログイン(LDAP / OIDC)はコミュニティ側から外れ、有償の AIStor 側の機能という整理になっています(STS API など、プログラムから組む余地は残っているとされます)。GUIで管理したい場合の選択肢は、AIStor へ移行するか、管理UIが残る最後のバージョン(2025-04-22 系)に固定して更新を止めるか、OpenMaxIO のようなコミュニティフォークやサードパーティUIに頼るか、のいずれか、という状況です。

出典: MinIO 公式リポジトリの議論(discussion #21326#21316)および各種報道。バージョンや挙動は変わり得るので、採用前に最新の状況を確認してください。

この「管理機能や認証が製品提供側の都合で出し入れされる」流れは、本記事の設計とも噛み合いません。後述するとおり、本構成は アプリ自身の認証機能に依存せず、認証を Cloudflare Access(Zero Trust)側で握る方針だからです。アプリのUIや認証仕様が変わっても、前段の守りは揺らぎません。

その上で Garage の良さは、

  • Rust製・単一バイナリで軽量、NAS規模のセルフホストに向く
  • 設定が TOML 一枚で完結
  • 分散指向だが単一ノードでも素直に動く
  • 管理は CLI が基本(GUIはサードパーティの garage-webui を使う) — もともと「UIに依存しない」設計なので、UI仕様変更に振り回されない

全体構成

                              ┌─────────────────────────── Cloudflare ───────────────────────────┐
[クライアント]                │                                                                   │
   │ https://s3.example.com   │  s3.example.com       ── Access: 許可IPを Bypass ──┐               │
   │ (S3署名 / presigned)     │                                                    │               │
   ▼                          │  s3-admin.example.com ── Access: 許可IP + SSO(Allow)┤               │
[Cloudflare Edge] ◀───────────┤                                                    │               │
                              └────────────────────────────────────────────────────┼───────────────┘
                                                                                    │ (outbound tunnel)
                                                                                    ▼
                                                          ┌──────── Synology DS918+ ────────┐
                                                          │  cloudflared (container)         │
                                                          │     ├─ → 192.0.2.10:3900 (S3 API)│
                                                          │     └─ → 192.0.2.10:3909 (WebUI) │
                                                          │  garage (container)  :3900/3903  │
                                                          │  garage-webui (container) :3909  │
                                                          └──────────────────────────────────┘

別ホスト(クラウド) :  portal.example.com (Bearer TOKEN) ── Garage の access/secret key を保持し presigned URL(10分)を発行

ポイントは2つ。

  1. cloudflared は1コンテナ。1本のトンネルに Public Hostname を2本(S3用 / 管理用)生やして出し分ける。
  2. 守りの段:
    • Cloudflare Access(S3=許可IPを Bypass する門番 / 管理=許可IP + SSO の認証本体)
    • Garage の S3署名(private bucket、匿名は 403) ← S3 API を実際に守る主体
    • (応用)自前 portal API が Bearer 認証の先で presigned URL を発行 → クライアントに生キーを渡さない

前提

  • DSM 7.x、Container Manager 導入済み
  • SSH 有効化済み
  • 独自ドメインを Cloudflare で管理済み(proxied)
  • Cloudflare Zero Trust(無料枠で可)
  • 固定グローバルIP(許可IP方式を恒久運用するため。動的IPだと後述のハマりが出る)
  • 外部 Docker ネットワーク qi-net を作成済み
sudo docker network create qi-net

1. ディレクトリ構成

sudo mkdir -p /volume1/portal/garage/{meta,data,conf}
sudo mkdir -p /volume1/portal/garage-container
sudo mkdir -p /volume1/portal/cloudflared-container

2. Garage の設定ファイル

RPC シークレットと管理APIトークンを生成します。

openssl rand -hex 32   # rpc_secret 用
openssl rand -hex 32   # admin_token 用

/volume1/portal/garage/conf/garage.toml:

metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"

replication_factor = 1

rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "(openssl で生成した64文字hex)"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.example.com"

[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.example.com"
index = "index.html"

[admin]
api_bind_addr = "[::]:3903"
admin_token = "(openssl で生成した64文字hex)"

[admin]admin_token は garage-webui が管理API(3903)へ接続するのに使います。

replication_factor = 1 は単一ノード構成なので、Garage 自体による冗長化はありません。 これは Garage 公式も「single-node は冗長性がなく production には推奨しない」と注意している点です。NAS の RAID / スナップショット / Hyper Backup / 別拠点バックアップとは別問題です。本記事は「外部から安全に S3互換 API と管理UI に到達する構成」であって、データ保全設計そのものではない、と理解してください。

3. docker-compose.yml(Garage + WebUI)

/volume1/portal/garage-container/docker-compose.yml:

version: "3.7"
services:
  garage:
    image: dxflrs/garage:v2.1.0
    container_name: garage
    restart: unless-stopped
    networks:
      - qi-net
    ports:
      # cloudflared を host IP 経由で繋ぐので S3 API だけ host publish。
      # 後述「より安全に」のとおり、cloudflared を qi-net に入れれば
      # これも publish 不要にできる。
      - "3900:3900"   # S3 API
    expose:
      - "3901"        # RPC: Docker network 内のみ。LAN に出さない
      - "3902"        # S3 web: 使わないなら出さない
      - "3903"        # admin API: webui からだけ使う。LAN に出さない
    volumes:
      - /volume1/portal/garage/meta:/var/lib/garage/meta
      - /volume1/portal/garage/data:/var/lib/garage/data
      - /volume1/portal/garage/conf/garage.toml:/etc/garage.toml:ro
    command: /garage server

  garage-webui:
    image: khairul169/garage-webui:latest
    container_name: garage-webui
    restart: unless-stopped
    networks:
      - qi-net
    ports:
      - "3909:3909"
    environment:
      API_BASE_URL: "http://garage:3903"      # qi-net 内でコンテナ名解決
      S3_ENDPOINT_URL: "http://garage:3900"
      API_ADMIN_KEY: "(garage.toml  admin_token と同じ値)"
      # defense in depth: Cloudflare Access に加えて WebUI 側にも簡易認証を置く
      # htpasswd -nbBC 10 "USER" "PASS" で生成したハッシュを設定
      # AUTH_USER_PASS: "admin:$2y$10$<bcrypt-hash>"
    volumes:
      - /volume1/portal/garage/conf/garage.toml:/etc/garage.toml:ro
    depends_on:
      - garage

networks:
  qi-net:
    external: true

LAN 側公開に注意。 ports: で publish したポートは、インターネットには出ていなくても Synology の LAN 側からは見えます。RPC(3901)/ S3 web(3902)/ admin API(3903)は expose(Docker ネットワーク内のみ)に留め、LAN に publish しないのが安全です。webui は同じ qi-net 内なので http://garage:3903 で admin API に届き、ホスト側に 3903 を出す必要はありません。

さらに堅くするなら、cloudflared も qi-net に入れて、Public Hostname の Service を http://garage:3900 / http://garage-webui:3909 に向ければ、S3 API の 3900 すら host publish せずに済みます(その場合 §6 の Service を LAN IP からコンテナ名に変更)。

ハマりどころ: API_ADMIN_KEY は garage.toml の [admin] admin_token完全一致させること。ズレると webui が 401 で管理APIに繋がりません。

起動:

cd /volume1/portal/garage-container
sudo docker compose up -d

4. ノード初期化(レイアウト割り当て)

sudo docker exec -it garage /garage status

表示されたノードIDにレイアウトを割り当てます。

sudo docker exec -it garage /garage layout assign -z dc1 -c 1G <node_id>
sudo docker exec -it garage /garage layout apply --version 1

CLI のサブコマンドや引数(特に layout assign のオプション)は Garage のバージョンで変わります。本記事は v2.1.0 系の例です。お使いのバージョンの garage --help / garage layout --help で確認してください。

5. アクセスキーとバケット作成

CLI でもできますが、後述の garage-webui からも GUI で管理できます。

sudo docker exec -it garage /garage key create my-key
sudo docker exec -it garage /garage bucket create my-bucket
sudo docker exec -it garage /garage bucket allow \
  --read --write --owner my-bucket --key my-key

key create で出る Key ID / Secret key を控えます(Secret は再表示不可)。この鍵が S3 アクセス制限の実体です。

6. cloudflared を1コンテナで立てる

/volume1/portal/cloudflared-container/docker-compose.yml:

services:
  cloudflare-tunnel:
    image: cloudflare/cloudflared:latest
    container_name: cloudflare-tunnel
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token ${CF_TUNNEL_TOKEN}

.env:

echo "CF_TUNNEL_TOKEN=(ダッシュボードのトークン)" > .env
chmod 600 .env

トークン方式では ingress ルート(どのホスト名→どのサービス)はダッシュボード側に保存されます。compose には書きません。

Public Hostname を2本生やす

Cloudflare Zero Trust → Networks → Tunnels → 該当トンネル → Public Hostname
このトンネル1本に、ルートを2本登録します。

# Hostname Service
1 s3-admin.example.com http://192.0.2.10:3909
2 s3.example.com http://192.0.2.10:3900

Service は 192.0.2.10(= Synology の LAN IP)直指定にしています。コンテナ名(http://garage:3900)解決でも動きますが、その場合は cloudflared を同じ qi-net に載せる必要があります。ホストIP直指定なら cloudflared を別ネットワークのまま運用できます(本構成はこちら)。

.env を置いたら起動:

cd /volume1/portal/cloudflared-container
sudo docker compose up -d

ダッシュボードでトンネルが HEALTHY になればOK。

7. Cloudflare Access でアクセス制御(本記事の核心)

ここからが本題です。S3 API と 管理UI で、Access の役割を変えます。

7-1. S3 API(s3.example.com)= 許可IPを Bypass

S3 API 側は Garage 自身が S3署名(SigV4)認証を持っているので、Access には認証させません。やることは「許可IPだけ通し、その先は Garage の署名に任せる」だけ。

Zero Trust → Access → Applications → Add an application → Self-hosted

  • 宛先(Public Hostname): s3.example.com
  • ポリシー: Action = Bypass、ルール = 送信元IP(203.0.113.10/32 など固定IP)

ここが今回いちばんハマった点。 ポリシーの Action を Allow にすると、IPが一致してもログイン(SSO)が要求され、curl や presigned 直DL が 302 でログイン画面に飛ばされます。

  • Allow = 条件に合う人を通すが ログインは要る
  • Bypass = 条件(IP/CIDR)に合えば ログイン無しで素通り

presigned URL での自動DL を通したいなら、S3 側は Bypass が正解。Bypass は Include にネットワークセレクタ(IP/CIDR)等しか混ぜられない制約があるので、IP単独ルールにします。

これで挙動はこうなります(実測):

アクセス 結果
許可IP・無認証で素のパス 403 AccessDenied(Garage の署名ゲートが効く。匿名拒否)
許可IP・presigned URL 200(正常DL)
許可外IP 302 → Cloudflare Access のSSOログインへ

「Access の 302 が消えて Garage の 403 が返る」=「Access を通過して Garage 本体まで到達した」の合図です。動作確認の判定に使えます。

Bypass の正確な意味を押さえておく。 Cloudflare 公式は Bypass について「Access の enforcement を無効化する」「Access のセキュリティ制御を適用しない」「リクエストはログに記録されない」と説明しています。つまり Bypass にした S3 側は、Access で「認証」しているわけではありません。 許可IPを Access の認証フローから外しているだけで、Access の認証・監査ログ・ユーザー識別は効きません。
この構成で S3 API を守っている主体は、あくまで Garage の SigV4 署名と private bucket です。Access の Bypass は「到達元IPを絞る外側の粗い門番」として使っているにすぎません。Cloudflare 公式も、内部アプリへ恒久的に直接アクセスを与える用途での Bypass を積極的には推奨していないので、「Access で守っている」と過信しないことが大事です。

ログインなしで「制御」と「監査ログ」を維持したいなら、Bypass ではなく Service Auth(サービストークン)や mTLS を検討してください。サービストークン(CF-Access-Client-Id / CF-Access-Client-Secret ヘッダ)なら、IPに依存せず・Access のログも残しつつ、自動化クライアントを通せます。固定IPが使えない/監査ログが要る恒久運用では、こちらのほうが筋が良い場面があります。

7-2. 管理UI(s3-admin.example.com)= 許可IP + SSO(Allow)

Garage の管理UI(garage-webui)は、未設定のままだと認証なしで起動できます。 素のままトンネルで出すと、URLを知っていれば誰でもバケットやキーを操作できてしまう。

そこで 認証層を Cloudflare Access に肩代わりさせます。(garage-webui 自身も AUTH_USER_PASS で簡易認証を足せるので、defense in depth として両方を併用するのが理想です。後述の compose 参照)

  • 宛先: s3-admin.example.com
  • ポリシー: Action = Allow、ルール = 送信元IP(許可IP)かつ 特定ユーザー(メールアドレス等で固定)
  • 認証: IDプロバイダ(SSO)を有効化、セッション期間は適宜(例: 24h)

S3 側と違い、ここは Bypass ではなく Allow にします。理由は明確で、

  • webui は 鍵そのものを発行・閲覧・削除できる画面なので、最も強く守るべき
  • IPだけ(Bypass)だと、許可IP圏内なら誰でもログイン無しで管理画面に入れてしまう
  • 「許可IP かつ SSOで特定ユーザー」の二重にして初めて安心

設計の対比(この記事で一番言いたいこと)

S3 API (3900) 管理UI (3909)
アプリ自身の認証 あり(S3署名) 既定で無(AUTH_USER_PASSで追加可)
Cloudflare Access の役割 素通り役(許可IPを通すだけ) 認証本体
Action Bypass Allow
守りの主体 Garage の SigV4 Cloudflare Access の SSO

「アプリが認証を持つか持たないか」で Access の使い方を変える — これが本構成の設計判断です。認証のないセルフホストアプリでも、ソースに一切手を入れず、Access のポリシーだけで安全に公開できます。

8. 動作確認(aws-cli)

aws configure set aws_access_key_id <Key ID>
aws configure set aws_secret_access_key <Secret>
aws configure set default.s3.addressing_style path

aws --endpoint-url https://s3.example.com --region garage s3 ls

echo hello > test.txt
aws --endpoint-url https://s3.example.com s3 cp test.txt s3://my-bucket/

許可IPから正しい鍵で操作でき、鍵なし/誤った鍵では 403 になることを確認できれば完成です。

9.(応用)生キーを配らない — portal API で presigned URL を発行する

ここまでで「許可IP + 署名」の公開 S3 は完成です。さらに一歩進めて、クライアントに access/secret key を一切渡さない運用にできます。

別ホスト(クラウド側)に自前の API サーバを置き、

  1. クライアントは API に Bearer TOKEN で認証してファイルIDを要求
  2. API は Garage の access/secret key をサーバ側だけで保持し、**presigned URL(有効期限10分)**を生成して返す
  3. クライアントはその presigned URL で Garage へ直接DL(s3.example.com)
[クライアント] ──Bearer──▶ [portal API(別ホスト)] ──access/secret key を保持──▶ presigned URL 発行
       │                                                                            │
       └────────────── 受け取った presigned URL(10分)で Garage へ直DL ─────────────┘
  • 生キーがクライアントに出ない(漏れても困るのは10分有効の署名URLだけ)
  • portal の入口は Bearer で守られ、Garage 本体は署名必須・匿名403

重要な落とし穴 — presigned 直DLは「クライアント自身のIP」が Access 条件を通る必要がある。
S3 側の Access ポリシーが「許可IPのみ Bypass」の場合、presigned URL を受け取って実際にアクセスするクライアントの送信元IPが許可IPに入っていないと、s3.example.com への直アクセスは Cloudflare 側で 302(SSO)に弾かれます。
つまり API サーバの固定IPを許可しただけでは「クライアント直DL」は成立しません。 API サーバが許可IPなのは、API サーバ自身が Garage に到達できる(presigned の発行検証や代理取得ができる)という意味であって、別IPの外部クライアントが直DLできることは意味しません。

外部クライアントにも配るなら、選択肢は次のいずれか:

  • portal API がファイルを proxy 配信する(クライアントは Garage に直アクセスせず、API 経由で受け取る)
  • クライアント側を VPN / 固定IP / Access 認証済み にして、Access 条件を満たさせる
  • S3 エンドポイントを SigV4 のみで公開(Access の IP 制限を外し、Garage の署名だけで守る)

本構成では「許可IP圏内のクライアント(自宅・拠点・API サーバ)」が前提です。不特定多数の外部に配るなら proxy 配信を選びます。

注意: presigned URL は短命とはいえ、それ自体が bearer credential(持っているだけで使える鍵)です。ただし本構成では Cloudflare Access の Bypass 条件(許可IP)も同時に満たす必要があるため、「URLを持てば誰でもDLできる」のではなく、「許可IP・Access認証済みブラウザ・proxy API など、Cloudflare 側を通過できる相手ならDLできる」と捉えてください。いずれにせよチャットやログに貼らないこと。

ハマりどころまとめ

  • Access の Allow と Bypass の取り違え: S3 の自動DL(presigned/curl)を通すなら Bypass。Allow だと IP一致でも SSO ログインを要求され 302。今回いちばんの罠。
  • 管理UIを Bypass にしない: webui は認証が無いので、IPだけの Bypass は危険。Allow + SSO + ユーザー固定で締める。
  • API_ADMIN_KEYadmin_token 不一致: webui が 3903 に繋がらない原因の筆頭。
  • path-style 強制: 仮想ホスト形式はワイルドカード証明書が絡むので、クライアントは path-style が無難(default.s3.addressing_style path)。
  • Cloudflare のアップロード上限: Cloudflare 経由のリクエストにはプランごとのボディ上限があり、Free / Pro は 1リクエストあたり約100MB(Business 200MB、Enterprise 500MB+)が目安です。これを超える PUT は前段で 413 で弾かれるので、大容量は S3 マルチパートアップロードを使い、各 part もこの上限未満に刻みます(ダウンロードはこの制限とは別の話)。
  • 動的IPだと許可IP方式が崩れる: 許可IPは固定IP前提。動的IPだと再接続でIPが変わり、Bypassルールとズレて 302 に戻る。恒久運用は固定IP / サービストークン / API代理 のいずれかで。
  • RPC(3901)/admin(3903)を外に晒さない: トンネルに生やすのは S3(3900)と webui(3909)だけ。admin/RPC は宅内限定に。

まとめ

DS918+ にルーターのポートを一切開けず、cloudflared 1コンテナの1トンネルから S3 API と管理UI を別ホスト名で出し分け、Cloudflare Access で

  • S3 API は許可IPを Bypass(認証は Garage の署名に任せる)
  • 認証のない管理UIは Allow + SSO(Access が認証本体)

という役割分担で守りました。「アプリが認証を持つかどうかで Zero Trust の使い方を変える」という設計が、そのまま再利用できる形になっています。restic / rclone のバックアップ先としても実用的です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?