Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Oct 29, 2026 - 日記

Syncthingの接続範囲を見直す:万が一に備える設定と運用負荷

Syncthingの接続範囲を見直す:万が一に備える設定と運用負荷

ファイル同期ソフトのSyncthing は、端末同士でファイルをやり取りするP2P型で、クラウドの保管サーバにファイルを預けずに使えます。 通信の暗号化と端末認証も備えており、データの置き場所や共有相手を自分で管理したい人に向いた設計です。

この記事は、現在のSyncthingで新たな脆弱性が見つかったことを受けた注意喚起ではありません。 その安全設計を前提に、万が一、認証前の通信処理に深刻な不具合が生じた場合に備え、用途に不要な接続経路をさらに減らす設定を考えます。

Syncthingの安全設計とクラウドに預けない利点

Syncthingのセキュリティ原則 では、安全性を主要な目標の一つとしています。 端末間の通信はTLSで暗号化され、接続時には相手の証明書を、あらかじめ許可した端末の情報と照合します。 端末の識別に使うDevice IDは、証明書から計算される識別子です。 P2Pという通信形態だけで安全になるのではなく、この暗号化と認証が通信を保護しています。 直接接続できずリレーを通る場合も、端末間の暗号化は維持され、リレーがファイルの中身を読める仕組みにはなっていません。

ファイルを第三者のクラウド保管サーバに置かないため、その保管先からの漏えいや、事業者による内容へのアクセスに伴うリスクを減らせます。 この点は、ファイルを事業者に預ける一般的なクラウドストレージと比べた利点です。 ただし、安全性はクラウド側の暗号化方式や、自分で管理する端末の更新状況によっても変わります。

この記事で扱う接続制限は、Syncthingの安全設計に加える万が一の備えです。 セキュリティやプライバシーを意識してSyncthingを選んだ人が、用途に応じて接続範囲も見直せるように整理します。 Syncthingを危険なソフトとして扱ったり、通常の利用者全員に設定変更を急がせたりする話ではありません。

公衆探索の照会範囲と公開ポートのリスク

ファイルの同期が端末間で行われても、相手端末の接続先を見つける仕組みには、公衆のサーバを使う場合があります。 外部の探索サーバを使うGlobal Discoveryは、標準設定で公衆探索サーバを利用します。 端末は探索サーバにDevice IDと接続先アドレスを対応づける情報を通知します。 探索先を私設サーバに置き換えた場合も、この機能を使います。 ここで扱うのは接続先の情報であり、同期するファイルそのものを探索サーバに預けるわけではありません。

この情報が、万が一の脆弱性を狙う相手に、攻撃対象の接続先を見つける手がかりを与える可能性を考えます。 探索サーバへの登録そのものが、相手に同期を許可したり、端末の認証を無効にしたりするわけではありません。

ただし、公衆探索の仕様 にある照会は、Device IDを指定してアドレスを取得するものです。 全ノードを一覧取得するAPIとして定義されているわけではありません。 「登録した全ノードが誰でも閲覧できる名簿になる」と考えるのは正確ではありません。

一方、Device IDを知っている相手は、公衆探索を通じてその端末の接続先アドレスを調べられます。 探索サーバの運営者も登録情報を扱います。 その意味で、公衆探索に渡す情報を減らしたい、という判断には理由があります。

公式FAQ は、Device IDを秘密にする必要はないと説明しています。 通常の同期接続では双方が相手のDevice IDを登録している必要があり、IDを知っただけでファイルを取得できるわけではありません。

ここで備えたいのは、その認証が正常に機能する状況だけではありません。 仮に認証前の通信処理に脆弱性があれば、正規の同期相手として承認されていなくても、到達できるポートに攻撃を試せる可能性があります。 また、同期ポートがインターネットに開いていれば、公衆探索を使わない走査でも見つかり得ます。 探索を止めることと、外から届かなくすることは別の対策です。

次の図は、探索で扱う接続情報と、実際の同期通信を分けたものです。 破線は接続情報、実線は同期通信または接続の試行を表します。

flowchart TB
    accTitle: 探索と同期通信の違い
    accDescr: 端末Aは探索サーバでBの接続先を照会し、同期通信はAとBの間を流れる。公開待受への接続試行は探索停止だけでは遮断できない。
    D["公衆探索サーバ
接続情報を扱う"] A["端末A"] B["端末B
公開した同期待受"] X["不特定の相手"] B -. "登録" .-> D A -. "Bを照会" .-> D A <-->|"暗号化した
ファイル同期"| B X -->|"公開ポートへ
接続を試行"| B

探索先の変更や探索停止は破線の経路を見直す対策です。 不特定の相手からの実線を遮断するには、待受とネットワーク側の制限が必要です。 図では直接同期を示し、リレー経由の通信は省略しています。 接続できることと、同期相手として認証されることも別です。

対策ごとの効果と運用負荷

公衆探索への通知を減らすことと、不特定の相手が同期ポートへ届かないようにすることでは、必要な設定が違います。 前者だけが目的なら探索の見直しから始められますが、後者には待受とネットワーク側の制御が必要です。 次の対応は、減らしたいリスクと残したい利便性で選び、組み合わせるものです。 安全性の点数順や、必ず最後まで進める段階を示すものではありません。

同期する端末がすべてTailscale のプライベートネットワーク(tailnet)に入っているなら、公衆探索や公衆リレーを使わずに接続できます。 ただし、使わない機能を止めるほど、端末追加や接続障害への対応を自分で引き受ける範囲も増えます。 用途に合う対策を比較し、私設サーバを使う構成とtailnet内に限定する構成を示します。 そこまで絞る必要があるかも、守りたいデータと運用負荷に応じて判断します。

効果と運用負荷は、少数の管理端末で使う場合を想定した、この記事での目安です。 実測した防御性能や、脆弱性を防げる確率を示す数値ではありません。

  • 情報提供の削減効果:Syncthingの公衆探索と公衆リレーへ渡す接続情報を減らす効果です。1は両方を使う、3は一方だけを使う、5は両方を使わない構成です。2と4は部分的に絞る場合の中間評価です。LAN内の通知やTailscale側への情報提供は別に扱います。
  • 到達制限の効果:同期接続の到達範囲(直接接続とリレー経由)を狭める効果です。1は変更なし、2は一部の経路を小さく制限、3は一部の経路を閉鎖、4は全経路を許可したネットワーク単位に限定、5は全経路を許可した端末単位に限定する構成です。探索処理自体の受信面を減らす効果は、この軸に含めません。5でも許可済み端末からの攻撃は防げません。
  • 運用負荷:標準構成に追加する設定、端末追加、更新、障害対応の負担です。1は小、2はやや小、3は中、4は大、5は最大です。Tailscaleや私設サーバを既に運用している場合は下がります。

最初の三案は公衆リレーを残し、後半三案は公衆探索と公衆リレーを両方外す前提です。 私設リレー案の到達制限は、直接待受を外し、私設サーバへの接続元もネットワーク側で制限する場合の評価です。 私設サーバを立てただけなら、到達範囲は狭まりません。 LANとtailnetの案も、ホスト側の受信制限とtailnetポリシーを適用する前提です。 到達制限の4と5は方式の優劣ではなく、許可範囲の違いです。 私設リレー案とLAN併用案は、許可ネットワーク内に同期相手以外の端末も含む想定で4としています。 私設リレーでも許可範囲を端末単位に絞れば5に相当し、tailnetでも広い許可を残せば5にはなりません。

選ぶ対応 情報提供の削減効果 到達制限の効果 運用負荷 得られる利便性と注意点
公衆探索とLocal Discoveryを止める 3 1 2 接続先を固定して管理する。公衆リレーは別判断で、公開待受への到達も変わらない
公衆探索を止め、Local Discoveryを残す 3 1 1 LAN内の自動発見を維持する。LAN内にはDevice IDなどを通知し、LAN外の接続先管理と待受制限は別途必要
探索先を私設サーバに置き換える 3 1 3 自動発見を維持する。探索サーバの更新、証明書、保存情報、利用者の制限を管理する。同期ポートの到達範囲は変わらない
私設探索と私設リレーだけを使い、接続元も制限する 5 4 5 直接接続できない端末も同期できる。サーバの稼働、帯域、利用者の制限を管理する。リレー停止中は同期できない
公衆探索と公衆リレーを止め、LANとtailnetに接続を絞る 5 4 4 LANでの直接同期を残せる。ホストとtailnetの許可範囲を管理し、許可したLAN端末からの到達も残る
公衆探索と公衆リレーを止め、許可したtailnet端末だけに限定する 5 5 4 対象端末へ届く相手を絞る。Tailscaleの稼働、固定アドレス、ホストの受信制限、tailnetポリシーを保守する

表中の効果と負荷は5段階評価です。効果は大きいほど強く、負荷は大きいほど重くなります。

探索先を私設サーバへ置き換える案でも、公衆リレーの扱いは別に決めます。 探索を止めても、公衆リレーへの参加登録を続けることはできますが、相手のリレー接続先を自動で取得できるとは限りません。 Global DiscoveryとLocal Discoveryを両方止める場合や、Local Discoveryだけでは発見できないLAN外の相手には、固定の直接接続先や相手のリレーURLなどを手動で指定する必要があります。 固定TCPアドレスだけを指定した構成では、直接接続できないときに公衆リレーへ自動で切り替わることは期待できません。 LAN外の端末が多い場合や、接続先が頻繁に変わる場合は、固定アドレス管理の負荷が上がります。 私設サーバの接続元を固定IPで絞れず、別のVPNを導入する場合は、その保守も負荷に加わります。 同じ点数でも、制限する経路や保守する対象は異なります。 点数を足して安全度を決めるものではありません。

Local Discoveryを残す案では、LAN内の同期相手について、Addressesのdynamicを残すと自動発見を続けられます。 一方、LAN外の相手には到達できる固定アドレスなどを用意します。 Local Discoveryは接続を許可する仕組みではないので、LAN内の自動発見を残すことと、LAN全体からの通信を許可することは分けて判断します。

リレーも、必要性を確認してから止めます。 外出先や制限の厳しいネットワークで直接接続できないとき、リレーを無効にすると同期できなくなる場合があります。 リレーは特定のサーバだけに絞る構成も選べます 。 自分で探索サーバとリレーを運用すれば、自動発見やNAT越えの利便性を残しながら、公衆サーバへの登録をやめられます。 次節では、その構成とアクセス制限を整理します。

Tailscaleを導入できない端末があるなら、公衆探索やリレーを残す判断もあり得ます。 その場合は、残す理由と到達範囲を把握したうえで、更新、管理画面の非公開化、不要な共有の解除、復元可能なバックアップを続けます。 既知の脆弱性が公表された場合は、影響条件に応じて停止や更新を判断します。 部分的な対策を、その対処の代わりにはしません。

私設探索サーバと私設リレーによる構成

全端末をtailnetに参加させず、公衆サーバへの登録をやめたい場合は、探索先とリレーを自分で用意する選択肢があります。 探索先だけを私設化する構成と、リレーも含めて私設化する構成は別々に選べます。 ただし、「自分が運用するサーバ」と「自分たちだけが接続できるサーバ」は同じではありません。 アクセス制限を設けずインターネットへ公開すれば、その私設サーバにも不特定の相手が到達できます。

探索と中継の役割分担

接続先のアドレスを通知し、Device IDを指定して照会するためのサーバが、stdiscosrv です。 同期するファイルを保管したり、通信を中継したりするものではありません。 公衆探索を私設探索に置き換えても、端末に公開した同期待受があれば、その到達範囲は変わりません。

直接接続できない端末同士の暗号化通信を中継するのが、strelaysrv です。 中継中も端末間の暗号化とDevice IDによる認証は維持されます。 リレーの運用者には接続する端末のIP、Device ID、通信量が分かります。 私設化は、その情報を扱う運用者も自分で選ぶための対策です。

次の図は、探索とリレーを私設化し、直接の同期待受を外した構成です。 破線は探索用の接続情報、実線はリレー経由の暗号化通信を表します。

flowchart TB
    accTitle: 私設探索と私設リレーの構成
    accDescr: AとBは私設探索へ登録と照会を行い、暗号化した同期通信は私設リレーを経由する。両サーバの受信経路には別途接続元制限を設ける。
    A["端末A"]
    B["端末B"]
    A -. "登録と照会" .-> D["私設探索
stdiscosrv / TCP 8443"] B -. "登録と照会" .-> D D ~~~ R["私設リレー
strelaysrv / TCP 22067"] R <-->|"暗号化通信"| C["端末Aと端末B
リレー経由で同期"]

探索サーバは接続情報だけを扱い、ファイル同期の通信はリレーを通ります。 暗号化は端末間で維持され、リレーがファイルの中身を読めるわけではありません。 上段は接続先を探す通信、下段はファイルを同期する通信です。 両サーバへの接続元制限は別途設け、Dockerで起動するだけでは制限されません。 探索だけを私設化する場合はリレーが不要なこともあり、端末間の直接接続は別途管理します。

サーバの起動と公開範囲

私設サーバはSyncthing公式のDockerイメージ を使うと、バイナリの取得や常駐設定を個別に行わずに立てられます。次はDocker Composeで探索とリレーを起動する例です。Docker EngineとComposeを使えるサーバを前提とし、バージョンを固定します。

192.0.2.10は説明用です。サーバ自身に割り当てられた、利用端末から到達できるIPに置き換えてください。Dockerのポート公開は接続元を認証しません。起動前にTCP 8443と22067を利用端末からだけ到達可能にします。LinuxではDockerの公開ポートがufwの規則を迂回する場合があります 。 利用中のバックエンドに応じ、Docker公式のiptables向け手順 またはnftables向け手順 に沿って、コンテナへの転送経路に効く受信制限を設けてください。 別ホストやルーターで制限する場合も、実際の受信経路全体を確認してください。 公開IPv6やDocker Desktopのポート転送経路も確認します。 固定IPで絞れない場合は別のVPNや閉域ネットワークも検討します。

以下をcompose.yamlとして保存します。証明書・秘密鍵・探索情報は名前付きボリュームに残り、コンテナを再作成しても保持されます。-pools=は公衆リレープールへの登録を止め、-status-srv=は統計用の待受を無効にし、-nat=falseはリレーによる自動ポート転送を止めます。探索だけならrelayサービスを起動せず、後述のリレー限定設定も適用しません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
services:
  discovery:
    image: syncthing/discosrv:2.1.7
    restart: unless-stopped
    environment:
      UMASK: "077"
    command: ["--listen=:8443", "--db-dir=/var/stdiscosrv"]
    ports:
      - "192.0.2.10:8443:8443/tcp"
    volumes:
      - discovery-data:/var/stdiscosrv
  relay:
    image: syncthing/relaysrv:2.1.7
    restart: unless-stopped
    environment:
      UMASK: "077"
    command: ["-keys=/var/strelaysrv", "-listen=:22067", "-pools=", "-status-srv=", "-nat=false"]
    ports:
      - "192.0.2.10:22067:22067/tcp"
    volumes:
      - relay-data:/var/strelaysrv

volumes:
  discovery-data:
  relay-data:

アクセス制限を設けてからdocker compose up -dで起動します。探索だけならdocker compose up -d discoveryとします。 起動直後、クライアントを設定する前に、許可した接続元から接続でき、許可していない接続元からはTCP 8443と22067へ到達できないことを確認してください。 探索だけを起動した場合はTCP 8443が対象です。 拒否を確認できなければdocker compose downで停止し、受信制限を見直します。 docker compose psとdocker compose logs discovery relayで起動状態と各サーバのDevice IDを確認します。 公衆プールへの登録停止はrelayの起動引数で点検します。

ログ上のリレーURLに0.0.0.0が出ても、それはコンテナ内の待受アドレスです。端末にはホストの到達可能なアドレスを指定します。discoveryの初回起動時にrecords.dbがないというログが出ても、その後Loaded databaseが表示されれば新規作成時の挙動です。

docker compose downでコンテナは止まりますが、ボリュームは残ります。down -vは鍵と保存情報を消すので実行しません。

両サーバの秘密鍵を含むボリュームはバックアップし、公開しないでください。鍵を作り直すとIDも変わるため、クライアント側の指定を更新する必要があります。同じホストでSyncthing本体も動かす場合は、探索サーバと本体を別IPに分けるか、その本体への接続先を固定します。これは公式の探索サーバ手順にも記載された注意点です。Dockerで起動しても、公開範囲と更新の管理は残ります。

クライアントの探索先とリレー指定

各端末で変更前の設定をバックアップし、新しい経路で同期を確認してから旧経路を外します。 次のDISCOVERY-SERVER-IDとRELAY-SERVER-IDは説明用です。 それぞれの起動ログに出たIDへ置き換えます。 同期相手のDevice IDとは別の値です。

私設探索を使う場合も、Global Discoveryは有効のままにします。 この設定名は公衆サーバ専用という意味ではありません。 Global Discovery Serversの一覧からdefaultを削除し、次のURLだけを指定します。 自己署名証明書を使うこの例では、?id=で探索サーバの証明書を照合します。

1
https://192.0.2.10:8443/?id=DISCOVERY-SERVER-ID

defaultを併記すると、公衆探索も引き続き使います。 正規のCA証明書を使う構成では、その証明書に対応するホスト名をURLに指定し、?id=は不要です。 ?id=はクライアントが接続先サーバを確認するための値であり、探索サーバの利用者を制限するパスワードではありません。 探索先を他者に知られないことに頼らず、受信制限を設けます。

探索だけを私設化し、端末同士が直接同期できるなら、ここまでの設定でリレーの新設は不要です。 その場合も、リモートデバイスのAddressesをdynamicにして私設探索を利用し、同期待受と受信制限は別に管理します。 公衆リレーを残すか止めるかは独立した判断であり、探索先を変えただけでリレーも私設化されたことにはなりません。

私設リレーを使う端末では、Enable Relayingを有効のままにします。 次は、直接の同期待受も設けず、私設リレーだけを使う例です。 Sync Protocol Listen Addressesの一覧全体を、次のURL一つに置き換えます。 default、公衆プールを指すdynamic+https://...、既存のTCP・QUIC待受は残しません。

1
relay://192.0.2.10:22067/?id=RELAY-SERVER-ID

この値を、同期する双方の端末に設定します。 リモートデバイスのAddressesはdynamicにして、私設探索から相手のリレー経由の接続先を取得します。 Local DiscoveryとEnable NAT traversalは無効にし、直接接続用の固定アドレスが残っていないかも確認します。 この構成はリレー停止中に同期できず、転送速度と帯域もリレーに依存します。 直接同期も残したい場合はTCPの待受を明示的に併記できますが、その受信制限は別途必要です。

私設リレーには、リレーへの参加登録を共有トークンで制限する-tokenもあります。 この指定は公衆プールへの参加も無効にし、クライアントはリレーURLの&token=で同じ値を渡します。 トークンはすべての中継要求を認証するものではないため、接続元の制限の代わりにはしません。 URLに含むトークンは設定ファイル、ログ、スクリーンショットから漏れ得るため、公開せず、十分に長いランダムな値を使います。 トークン照合前の通信処理やサーバへの到達自体は残るため、未知の脆弱性への備えとしてネットワーク側の制限も続けます。

私設化後の確認と運用負荷

設定を保存した後は、対象リモートデバイスを一時停止して再開し、既存接続を張り直します。 探索先の一覧に公衆サーバが残っていないことを確認します。 私設探索への登録と照会が成功し、接続詳細に想定した私設リレーが表示され、テストファイルが同期することを確かめます。 サーバ側でも、許可していない接続元から8443と22067へ届かないことを試験します。 私設リレーを止める試験では、公衆リレーや直接接続へ意図せず切り替わらないことも確認します。

公衆リレープールに載せない設定は、リレーの所在を公開しないためのもので、接続元の制限ではありません。 また、リレー経由の接続は端末が外向きに張った通信を利用するため、端末の受信ポートを閉じるだけでは中継経由の到達を止められません。 私設サーバの利用者と直接経路の両方を管理して初めて、想定する相手に接続範囲を絞れます。

保守する対象は、Syncthing本体に加えて探索サーバ、リレー、そのOS、証明書や鍵になります。 探索情報の保存先やログの保護、更新、稼働監視、リレーの帯域費用も引き受けます。 私設探索が停止すれば新しい接続先を発見できず、私設リレーが停止すればそれを必要とする同期が止まります。 私設サーバ自体も外部から通信を受けるソフトウェアなので、既知の問題への更新や停止の判断は必要です。 少数の端末すべてをtailnetに入れられるなら、次の固定アドレス構成の方が、Syncthing専用のサーバを二つ増やさずに済みます。

tailnet限定へ向けた段階的な移行

全端末を同時に変更する必要はありません。 たとえば常時稼働する保管用端末と、管理しやすい同期相手の一組から始め、端末追加と障害対応を続けられるか確認します。 ただし、保管用端末をtailnet内限定にするなら、その端末と直接同期する相手は、すべてtailnet経由で届く必要があります。 一つでも公衆リレーなどの例外を残せば、その端末は厳密なtailnet内限定ではありません。

Global Discovery、Local Discovery、リレー、同期待受は、Syncthingのプロセス全体に対する設定です。 リモートデバイスごとの接続先Addressesや、フォルダごとの共有設定とは作用する範囲が違います。 同じプロセス内で「重要なフォルダだけtailnet限定」にしたつもりでも、他の共有用に公開待受を残せば、プロセス自体の到達範囲は狭まりません。 端末ごとに方針を分ける場合も、制限する側とその同期相手の双方で接続条件を確認します。

移行では、まず新しい経路で同期が成立することを確認し、次に不要な探索やリレーを止め、待受と接続許可を絞ります。 一度の変更を小さくすると、同期が切れたときに原因を切り分けやすくなります。 OSの再起動、LAN外からの利用、端末追加も確認してから対象を広げます。

継続して負担になるのは、接続先の更新、Tailscale停止時の切り分け、ファイアウォールとtailnetポリシーの変更確認です。 端末の再登録などでIPが変わった場合は、固定した接続先も更新します。 変更前の設定と、許可を残す理由を記録しておくと、担当者が替わっても判断を引き継げます。 同期が止まっても、原因を確認せず待受を全インターフェースへ戻したり、全許可ルールを追加したりはしません。 元の設定へ戻す場合も、公開範囲が広がることを理解し、許容できる状態かを確認します。 許容できる場合に限り、記録した変更を逆順に一項目ずつ戻し、同期と到達範囲を再確認します。 設定ファイルを復元する場合は対象のSyncthingを停止し、同じ端末のバックアップを使ってから起動します。

tailnet限定構成の設定例

ここからは、前半で比較した対応のうちtailnet内限定を選んだ場合の構成例です。 対象は、自分や組織が管理する少数の端末同士で同期し、すべての端末でTailscaleを動かせる用途です。 Tailscaleを導入できない端末との同期や、公衆リレーを必要とする運用では、そのまま適用できません。 以下はLinux向けの構成案で、ホスト側の受信制限を別途構築できることが前提です。 実測はループバック上の固定TCP同期までで、tailnetへの適用と外部からの拒否は確認していません。

今回は設定を確認しやすくするため、Syncthingの同期通信をTCPだけにします。 QUICが危険だからではありません。 UDPの同期待受を増やさず、必要な経路を一つに揃えるためです。

この構成では、次の三つの対策を組み合わせます。

対策 変更すること それだけでは防げないこと
探索とリレーを止める 公衆探索、ローカル探索、Syncthingのリレーを無効化 既知のIPへの直接接続
待受と受信元を限定する 自端末のTailscale IPで待ち受け、ホスト側でも受信インターフェースを制限 tailnet内の別端末からの接続
接続許可を限定する 必要な端末間のTCP 22000だけを許可 許可済み端末の侵害

以下のIPは説明用です。 実際に割り当てられた各端末のTailscale IPv4へ置き換えてください。 100.64.0.0/10全体を信頼する設定にはしません。

端末 Tailscale IPv4 同期相手
A 100.101.102.103 Bだけ
B 100.104.105.106 Aだけ

次の図は、AとBの同期だけを許可する構成の目標です。 同じtailnet内の端末にも、同期ポートへの接続を一律には許可しません。

flowchart TB
    accTitle: tailnet限定構成の許可と拒否
    accDescr: AとBのTCP 22000を相互に許可する。未許可のtailnet端末はgrantsで、tailnetを経由しない通信はホスト側で拒否する構成目標。
    subgraph T["tailnet"]
        A["端末A
100.101.102.103"] B["端末B
100.104.105.106"] C["許可していない端末C"] G["AとBのTCP 22000宛てを
grantsで拒否"] A <-->|"TCP 22000だけ許可"| B C --> G B ~~~ C end L["tailnetを経由しない通信
LANや外部回線"] H["ホストの受信制限で拒否
AとBの同期待受を保護"] G ~~~ L L --> H

図の拒否は設定の目標であり、検証済みの結果ではありません。 Tailscale経由はgrants、経由しない通信はホスト側の制限で扱います。 Syncthingの探索とリレーは使いませんが、下位のTailscale通信はDERPなどで中継される場合があります。 待受をTailscale IPへ固定するだけでは、受信経路の制限は完了しません。

待受を特定IPへ固定する例は、Linux上の通常のTailscaleネットワークインターフェースを前提とします。 コンテナ、ユーザー空間ネットワーク、OS別のVPN実装では、SyncthingのプロセスからそのIPで待ち受けられるかを先に確認してください。 待受に失敗しても、原因を確認せず0.0.0.0へ広げないでください。

ホスト側の受信インターフェース制限

IPへの待受固定は、受信インターフェースを限定する設定ではありません。 LinuxではIPアドレスはインターフェース単体ではなくホスト全体に属します 。 そのため、ホストのファイアウォールでも同期用TCP 22000をtailscale0経由に限定し、それ以外のインターフェースから届く通信を拒否します。 これはLANなどからTailscale IP宛てに直接送られる通信への備えでもあります。 既存のTailscale用ルールを壊さず、利用中のファイアウォールで設定と適用順を確認してください。 宛先IPの限定だけを、tailnet経由であることの保証にはしません。

この制限をホスト側で構築できることを、以降の設定を使う前提とします。 具体的なファイアウォールの変更コマンドは、既存の管理方式と競合し得るため、この記事では示しません。 受信インターフェースを制限できていない場合は、Syncthingとgrantsだけを変更しても「tailnet内限定」の設定は完了していません。

Syncthingの接続先、探索、待受の設定

変更前に、各端末の設定をバックアップします。 設定ディレクトリには秘密鍵も含まれるため、バックアップを公開したり、別端末の稼働用設定へ上書きしたりしないでください。 通信設定の変更で一時的に同期が切れるため、双方の管理画面へ入れる状態で作業します。

設定名と意味は公式の設定リファレンス で確認できます。 以下は同リファレンスとv2.1.2の設定画面ソース に基づく例です。 表示言語や配布形態によって画面の表記は変わります。

1. Tailscale IPによる接続先の固定

まず各リモートデバイスの編集画面で、Addressesのdynamicを削除し、相手のTailscale IPを指定します。 A側にはBのアドレス、B側にはAのアドレスを入れます。 固定値にdynamicを併記すると、探索による候補も使う指定になるため、今回は残しません。

A側に入力する値は次のとおりです。 保存後の接続先がBのTailscale IPになることを確認します。

1
tcp://100.104.105.106:22000

B側には、相手であるAのアドレスを入力します。 Device IDとフォルダ共有の設定は引き続き必要です。

1
tcp://100.101.102.103:22000

接続できない場合は、Tailscaleの接続状態、既存のアクセス制御、ホストのファイアウォールを先に確認します。 旧経路の接続が残る場合は、対象のリモートデバイスを一時停止して再開し、接続をやり直します。 その後、接続先とファイル同期を確認します。 この段階でTailscale経由の同期が成立してから、探索などを止めます。

2. 探索、リレー、NAT越えの無効化

各端末のSettingsにあるConnectionsで、次の四つを無効にします。 括弧内は設定ファイルの項目名です。

設定画面の項目 値
Global Discovery(globalAnnounceEnabled) false
Local Discovery(localAnnounceEnabled) false
Enable Relaying(relaysEnabled) false
Enable NAT traversal(natEnabled) false

Global Discoveryを無効にすると、外部の探索サーバへの通知と照会が止まります。 Local Discoveryを無効にすると、LAN内への探索情報の送信と、その情報を使った相手の発見が止まります。 固定アドレスを使うので、どちらも不要です。

Syncthingのリレーも使いません。 直接通信が難しいネットワークでの中継は、Tailscale側に任せます。 SyncthingのNAT traversalも止め、Syncthing自身によるUPnPやNAT-PMPのポート転送要求を行わない設定にします。 これはTailscale自身のNAT越え機能を止める設定ではありません。

3. Tailscale IPだけでの同期待受

Sync Protocol Listen Addressesのdefaultを、明示したTCPアドレス一つに置き換えます。 defaultはTCPとQUICの全インターフェース待受や動的リレーの指定を含むため、残さないようにします。

A側の待受には、自端末であるAのIPを指定します。 保存し、必要なら画面の案内に従って再起動した後、待受エラーがないことを確認します。

1
tcp://100.101.102.103:22000

B側の待受には、自端末であるBのIPを指定します。 接続先の設定とはIPが逆になる点に注意してください。

1
tcp://100.104.105.106:22000

Tailscaleの準備前にSyncthingが起動すると、指定IPで待ち受けられない場合があります。 OSのサービス管理で起動順を整え、再起動後にも接続を確認します。

GUIの待受は、同期用とは別です。 管理画面とREST APIは127.0.0.1:8384に保ち、必要なときだけSSHトンネルなどで利用します。 同期用のTCP 22000を許可しても、管理用の8384を開く必要はありません。

Tailscale grantsによる同期相手の制限

tailnetに参加している端末を、すべて同じように信頼する必要はありません。 AとB以外にも接続を許すと、その端末が侵害された際にSyncthingへの攻撃を試される可能性があります。

Tailscaleのgrants で、必要な端末間のTCP 22000だけを許可します。 許可は加算されるため、狭いルールを追加しても、既存の全許可ルールを打ち消せません。 従来のaclsも含め、対象ポートを広く許可するルールが残っていないか確認します。

以下はAとBが互いに接続を開始できるようにした、説明用のポリシーです。 既存ポリシー全体の置き換え用ではありません。 現在の管理用接続を維持したまま必要部分を統合します。 管理コンソールの検証で許可と拒否を確認してから適用してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
  "hosts": {
    "sync-a": "100.101.102.103",
    "sync-b": "100.104.105.106"
  },
  "grants": [
    {
      "src": ["sync-a"],
      "dst": ["sync-b"],
      "ip": ["tcp:22000"]
    },
    {
      "src": ["sync-b"],
      "dst": ["sync-a"],
      "ip": ["tcp:22000"]
    }
  ],
  "tests": [
    {
      "src": "100.101.102.103",
      "proto": "tcp",
      "accept": ["sync-b:22000"],
      "deny": ["sync-b:8384"]
    },
    {
      "src": "100.104.105.106",
      "proto": "tcp",
      "accept": ["sync-a:22000"],
      "deny": ["sync-a:8384"]
    }
  ]
}

ポリシーテスト のtestsにあるdenyは、通信を拒否するルールではありません。 その通信が許可されていないことを検証する期待値です。 さらに実在する別のtailnet端末のIPアドレスをsrcに指定したテストを加え、AとBの22000へ接続できないことも確認します。

この制御はTailscaleを経由する通信に対するものです。 LANや公開IPへの直接通信まで制限するものではないため、待受アドレスの限定も必要になります。

待受、同期、拒否の確認手順

以下は導入先で行う確認手順であり、この記事の実測結果ではありません。 実測したのは後述するループバック2ノードでの同期だけです。

設定を保存しただけで完了とせず、待受、接続、拒否をそれぞれ確認します。 Linuxでは、対象ホストでssを使って待受を調べられます。 次のコマンドは設定を変えず、TCPとUDPの待受を表示します。

1
ss -lntup

Syncthingが自端末のTailscale IPのTCP 22000だけで待ち受けていることを確認します。 0.0.0.0や[::]の待受が残っていないことも確認します。 今回の構成では、SyncthingのUDP 22000と探索用UDP 21027も不要です。 GUIの8384はループバックだけになっていることを確認してください。 プロセス名が見えない場合は、権限のある管理者が確認します。

次に、許可済みの相手から接続できることを確かめます。 Syncthingのデバイス詳細で、接続先が相手のTailscale IPであることを確認し、テスト用ファイルの作成と更新が同期されるかを見ます。 Tailscaleのping成功だけでは、TCP 22000の許可やファイル同期の成功までは確認できません。

拒否側も、次の条件を分けて確認します。 接続試験は、自分が管理する端末のうち、試験を許可されたものだけを対象にしてください。

試験元 試験先 期待する結果
許可していないtailnet端末 AまたはBのTailscale IPのTCP 22000 接続できない
tailnetを経由しないLAN端末 AまたはBのLAN IPのTCP 22000 接続できない
tailnetを経由しないLAN端末 AまたはBのTailscale IPのTCP 22000(LAN経由) 接続できない
tailnet外の回線 自分の公開IPv4と公開IPv6の同期ポート 接続できない

ファイアウォールの公式説明 にある通常の構成では、TCP 22000とUDP 22000を同期に使います。 今回使わないUDPも含め、ルータの手動転送、過去のUPnPマッピング、ホスト側の広い許可を点検します。 natEnabled=falseに変えただけで、既存の転送がすべて消えたとは判断しません。 公開IPv6ではIPv4のNATが防壁になると期待できないため、別に確認が必要です。

コンテナのポート公開やリバースプロキシなど、外部から中継する設定があれば、それも点検します。 別のプロセスが外部通信を転送していれば、Syncthingだけの待受確認では、その公開経路を見落とします。

Linuxの試験元では、先に次のコマンドで宛先までの経路を表示します。 LAN経由の試験には、宛先のTailscale IPへLAN側から到達する試験用経路を別途用意する必要があります。 その経路を用意できない場合、この条件の拒否を確認済みにはしません。

1
ip route get 100.101.102.103

LAN経由の試験でdev tailscale0が表示された場合は、意図した経路ではありません。 LAN側のインターフェースと次ホップを確認したうえで、接続を試します。 次はTCP接続だけを試す例です。 試験元にncがあり、-zと-wを使えることを前提にしています。

1
nc -z -v -w 3 100.101.102.103 22000

接続成功なら拒否設定を見直します。 接続失敗だけでは、ファイアウォールで拒否したのか、経路がないのかを区別できません。 宛先ホストの受信状況とファイアウォールのカウンタやログも照合してください。

固定TCP同期と私設サーバの実測範囲

設定の基本動作は、macOS上のSyncthing v2.1.2で確認しました。 既存の稼働設定には触れず、一時ディレクトリで2ノードを起動しています。 それぞれに別々の設定、鍵、同期フォルダを用意しました。

両方でGlobal Discovery、Local Discovery、Relaying、NAT traversalを無効化し、ループバックの固定TCPアドレスだけを指定しました。 TCP接続が成立し、テストファイルが同期されました。 送受信した内容のSHA-256も一致しました。 実行中のAPIから四つの設定がすべてfalseであることも読み戻しています。

この試験で確認したのは、探索とリレーがなくても固定アドレスで同期できることです。 本文のTailscale IPでの待受、ホストのファイアウォール、grantsの実適用、LANや外部回線からの拒否は、この試験では確認していません。 導入先では、前節の確認を行ってください。

別の隔離試験では、stdiscosrvとstrelaysrvのv2.1.7を公式配布のチェックサムと照合し、同じmacOS上で起動しました。 サーバはループバックの高位ポートだけで待ち受け、Syncthingの2ノードには私設探索と私設リレーだけを設定しました。 APIで設定を読み戻し、Global DiscoveryとRelayingが有効、Local DiscoveryとNAT traversalが無効であることを確認しました。 相手のAddressesもdynamicでした。 私設探索で得た接続先を使ってリレー接続が成立し、64 KiBのファイルを同期できました。 送受信した内容のSHA-256が一致し、接続種別もrelay-serverでした。 片方のリレートークンを誤った値へ変更すると、参加登録がwrong tokenで拒否され、新しいテストファイルは届きませんでした。

この試験では、探索サーバがループバックIPを含むリレーの通知を受け付けなかったため、リレーURLにlocalhostを使いました。 また、両ノードの登録を待ってから接続を開始しています。 初回照会時に相手が未登録だと、再照会まで待たされる場合があります。 ループバック用の調整を含む試験であり、公開IPでの運用、外部からの拒否、サービスとしての自動起動、長期稼働を実証したものではありません。 試験用のプロセスはすべて停止し、既存の稼働設定は変更していません。

接続制限後に残るリスクと継続的な備え

tailnet限定構成では、Syncthingへ直接接続できる相手を、許可した端末に絞ります。 許可した同期相手が侵害された場合や、同期するファイルの処理に問題がある場合のリスクは残ります。 SyncthingとTailscaleの更新、不要な共有の解除、設定と秘密鍵の保護は続けます。

また、「tailnet内限定」はインターネットを一切使わないという意味ではありません。 Tailscaleは制御用サーバと通信し、直接接続できない場合はDERPなどの中継 を使います。 SyncthingでTCPの直接接続と表示されていても、その下のTailscale通信が中継されている場合があります。

同期は削除や破損も伝え得るため、別に復元可能なバックアップを持ちます。 脆弱性の告知が出た場合も、影響条件に応じて停止や更新を判断し、「tailnet内だから対応不要」とは考えません。

すべての用途で同じ設定に揃える必要はありません。 部分的な対策なら残る到達範囲を把握し、厳密な限定が必要な端末には必要な対策を揃えます。 その区別を保ち、運用を続けられる範囲で対策を進めます。