Daydreaming in Boston

自宅サーバー上の写真を安全にインターネットから見られるようにする

1. はじめに

以前のエントリ でホームラボサーバーを建て、Google Photosの代わりとなる Immich という写真アプリを入れましたが、Google Photos置き換えとしては大事な点が抜けていました。それは外出先からインターネットを経由してのアクセスです。

今回は、Immichをインターネットからアクセスできるようにします。

2. リスクと対策

2.1. リスク

サーバーをインターネットに公開することは結構危険なことです。今回、トラブルシュート中に(caddyの)ログを見ていて気がついたのですが、ドメインに自宅ルーターのIPアドレスを設定して割とすぐにサーチエンジンyandex.comのボットからのアクセスが来ていました。(一部情報伏せてます)

{"level":"error","ts":1760900112.789462,"logger":"http.log.error","msg":"dial tcp :<port>: connect: connection refused","request":{"remote_ip":"5.196.80.240","remote_port":"44506","client_ip":"5.196.80.240","proto":"HTTP/1.1","method":"GET","host":"<URL>","uri":"/.git/config","headers":{"User-Agent":["YandexBot/3.0 (+http://yandex.com/bots)"],"Accept":["*/*"],"Accept-Encoding":["gzip"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"","server_name":"<URL>"}},"duration":0.000435061,"status":502,"err_id":"00vrh2gtu","err_trace":"reverseproxy.statusError (reverseproxy.go:1390)"}

また、leakixのIPアドレス(157.230.19.140)から脆弱性をチェックしていると思われるプローブが。。。

{"level":"error","ts":1760900122.5479143,"logger":"http.log.error","msg":"dial tcp :<port>: connect: connection refused","request":{"remote_ip":"157.230.19.140","remote_port":"60512","client_ip":"157.230.19.140","proto":"HTTP/1.1","method":"GET","host":"<URL>","uri":"/","headers":{"Connection":["close"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"","server_name":"<URL>"}},"duration":0.000433301,"status":502,"err_id":"9qcgybyv3","err_trace":"reverseproxy.statusError (reverseproxy.go:1390)"}

まあ、これらは危険では無さそうですが、こいつ(51.81.46.212)はやばそうです。

{"level":"error","ts":1760900548.6968164,"logger":"http.log.error","msg":"dial tcp :<port>: connect: connection refused","request":{"remote_ip":"51.81.46.212","remote_port":"56230","client_ip":"51.81.46.212","proto":"HTTP/1.1","method":"GET","host":"<URL>","uri":"/","headers":{"X-Scanned-By":["RecordedFuture-Global Inventory"],"Accept-Encoding":["identity"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"","server_name":"<URL>"}},"duration":0.00049338,"status":502,"err_id":"mmkg4vicx","err_trace":"reverseproxy.statusError (reverseproxy.go:1390)"}

IPアドレスで検索すると、、、

The IP address \(51.81.46.212\) is associated with the internet service provider OVH US LLC, a data center and web hosting company. This IP has been flagged for being used in malicious activities like phishing and other cyberattacks.

悪いことをたくらんでいるらしい会社のクローラーからのアクセスでした。他にも正体不明なIPアドレスからのアクセスがたくさん。。。

セキュリティをよく検討せずにサーバーをインターネットに公開してしまうと、ハッカーの標的になって、サーバーを乗っ取られたり、情報が盗まれるリスクがあるわけです。このことは知識として知ってはいても、実際にクローラーからのアクセスが来たのを見ると、リスクにさらされていることを実感します。

2.2. 対策

今回はcaddyのリバースプロキシー機能を使うことにしました。インターネットからのアクセスはすべてcaddyで受け、Immichを直接インターネットに晒さないようにします。Immichはhttpsをサポートしていないので、caddyでhttps対応します。

また、インターネットとLANをつなぐTP-Linkのルーターでポートフォワーディング設定し、443ポートのみをcaddyにつなぎます。必要のないポートはすべてルーターでブロックします。

リバースプロキシー以外にもVPNを使ったりできるようですが、将来的にサーバー上のWebサイトを公開することを見据えて茨の道を進んでみることにしました。

3. Immichをインターネットにexposeする

3.1. ドメインの取得

実は、(Wifi)ルーターをからインターネットにアクセスできるようにした時点で、インターネットプロバイダーはルーターにグローバルなIPアドレスを割り当てています。このIPアドレスはインターネット経由で全世界からアクセス可能です。もっとも、普段はルーターが外からのアクセスをすべてブロックしているわけですが。

IPアドレスはランダムに見える数字の羅列なので、人間が使いやすいようにドメイン名を使ってサービスにアクセスすることが普通です。インターネットに公開する自宅サーバーにもドメインが欲しくなるわけで、それを入手する必要があります。

ネットで調べたところ、mydns.jpという個人サイト(?)から無償でドメイン入手が可能なようだったのでアカウントを作ろうとしたのですが、いつまで待ってもアカウント登録メールが来ないので断念して、有償で業者から買い求めることにしました。

https://porkbun.comが安くて評判が良さそうだったので、ここで安いドメインを探しました。co.ukドメインがキャンペーン中で初年度$2.84、更新が$5.66と格安で出ていました。アメリカに住んでいる日本人なのにco.ukドメインはちょっと変な気もしますが、安いのでお試しにピッタリということで、一つドメインを購入しました。

なお、ドメインを購入するとそれが公開されるためか、ボット等からのクロールが始まります。IPアドレスを設定するまではporkbunが、IPアドレスを設定した後は自宅ルーターがブロックしてくれているはずです。

3.2. DDNSを設定する

インターネットプロバイダーは、固定IPアドレス契約をしていない限り、契約者のルーターに割り当てるIPアドレスを時折変更します。その際に、購入したドメインに割り当てるIPアドレスも変更する必要があり、それを自動でやってくれるのがDynamic DNS(DDNS)という技術です。

porkbun-ddnsはporkbunのAPIを使ってDDNS設定を自動でやってくれるありがたいツールです。300秒に一度、ルーターに割り当てられているグローバルIPアドレスをporkbunのDNS設定に反映してくれます。ものすごく便利!

インストールにはもちろんdocker composeを使います。githubのREADMEからdocker-compose.ymlサンプルをコピペしてきてDOMAIN, SUBDOMAINS, SECRETAPIKEY, APIKEYを入れます。DOMAINは購入したドメインでサブドメインは自分で使いたいものをカンマで区切って並べます。SECRETAPIKEYとAPIKEYはporkbunの管理画面で生成します。

services:
  porkbun-ddns:
    image: "mietzen/porkbun-ddns:latest"
    container_name: porkbun-ddns
    environment:
      DOMAIN: "<ドメイン>" # Your Porkbun domain
      SUBDOMAINS: "<サブドメイン>" # Subdomains comma spreaded
      SECRETAPIKEY: "sk1_<シークレットキー>" # Your Porkbun Secret-API-Key
      APIKEY: "pk1_<APIキー>" # Your Porkbun API-Key
      # PUBLIC_IPS: "1.2.3.4,2001:043e::1" # Set if you got static IP's
      # FRITZBOX: "192.168.178.1" # Use Fritz!BOX to obtain Public IP's
      # SLEEP: "300" # Seconds to sleep between DynDNS runs
      # IPV4: "TRUE" # Set IPv4 address
      # IPV6: "TRUE" # Set IPv6 address
      # DEBUG: "FALSE" # DEBUG LOGGING
    restart: unless-stopped

# # Uncomment below to let it detect ipv6 address:
#     networks:
#       - ipv6_enabled

# networks:
#   ipv6_enabled:
#     enable_ipv6: true

そうしたら、

docker compose up -d

でインストール、起動するだけ。難しいことは何も考えなくて済みます。

3.3. ルーターの設定

いよいよルーターでポートフォワーディング設定してポート番号443をこれからインストールするcaddyにフォワードします。うちのTP-Linkルーターの場合、ブラウザーからhttp://192.168.0.1 に行き、管理画面にログインしてAdvancedタブを選択し、NAT forwarding > Virtual Serversと進んでAddをクリックします。そしてExternal Portに443、Intrnal IPに自宅サーバーのIPアドレス、Internal Portに443を入力してOKを押します。

これで(ハッカーの攻撃を含めて)うちのルーターのグローバルIPアドレス、ポート443へのアクセスが全て自宅サーバーのポート443に来ているはずです。怖い怖い。。。

3.4. Caddyのインストール

最後にCaddyを入れてリバースプロキシーの設定をします。Caddyのすごいところは、httpsに必要なサーバー証明書を自動でLet's Encryptから生成、取得して、期限が切れる前に更新までしてくれるところです。面倒な証明書更新までお任せでやってくれるとは。感動的です。

Caddyももちろんdocker composeを使ってお手軽にインストールします。Caddy+Immich設定方法はImmichのドキュメントに出ています。

まずCaddyをデプロイするためのdocker-compose.ymlファイルを作ります。ポート80はブロックして、443のみを扱います。volumesのディレクトリはあなたのサーバー環境に合わせて適宜設定してください。

services:
  caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
        #- "80:80"
        - "443:443"
        #- "443:443/udp" # For HTTP/3
    volumes:
      - ./conf:/etc/caddy # Mount your Caddyfile for configuration
      - ./site:/srv # Mount directory for serving static files
      - caddy_data:/data # Named volume for Caddy's persistent data
      - caddy_config:/config # Named volume for Caddy's configuration

volumes:
  caddy_data:
  caddy_config:

ついでCaddyのconfigファイルを作ります。./conf/Caddyfileです。

<Immich用サブとメイン>.<取得したドメイン> {
    reverse_proxy 192.168.0.<snip>:2283
}

そして、

docker compose up -d

うまくいくとメッセージいろいろと出て動きます。

失敗したらCaddyのログを見に行きましょう。

$ docker ps | grep caddy  # CaddyのコンテナIDを表示
369b0b77d4ad   caddy:latest

$ docker logs 369b0b77d4ad 2>&1 | less  # コンテナIDを指定してログ表示

2>&1 の部分はログをlessで見られるようにするおまじないです。(標準エラー出力を標準出力にリダイレクトしています。)

例えば、

{"level":"error","ts":1760900129.7842133,"logger":"http.log.error","msg":"dial tcp :2283: connect: connection refused","request":{"remote_ip":"157.230.19.140","remote_port":"32786","client_ip":"157.230.19.140","proto":"HTTP/1.1","method":"GET","host":"<設定したサブドメイン>.<取得したドメイン>","uri":"/server-status","headers":{"Accept-Encoding":["gzip"],"Connection":["close"],"User-Agent":["Go-http-client/1.1"]},"tls":{"resumed":true,"version":772,"cipher_suite":4867,"proto":"","server_name":"photos.achiwa.co.uk"}},"duration":0.000412908,"status":502,"err_id":"2gb8awcmr","err_trace":"reverseproxy.statusError (reverseproxy.go:1390)"}

のエラーはHTTP 502 (Bad Gateway)であることがわかります。CaddyがうまくバックエンドのImmichと通信できないために出ているエラーです。client IP 157.230.19.140からのクロールがきっかけです。 https://www.abuseipdb.com/whois/157.230.19.140によると、DigitalOceanのドイツにあるデータセンター上でホストしているleakix.orgのスキャンサーバーからであるようです。

3.5. テスト

各コンテナのデプロイがうまくいっているようだったら、スマホのWifiを切って、ブラウザーからhttps://<サブドメイン>.<取得したドメイン> に行ってみます。無事にImmichのログイン画面が出てきたときには感動しました。ハッカーの侵入を防ぐ最後の砦であるImmichのパスワードは強めのものを設定しておきました。

証明書はLet's Encryptのものが正しく設定されています。

もしうまくいかなかったらコンテナのログを見たり、設定ファイルを変えるなどしてトラブルシュートします。

3.6. おまけ:あせったこと

いろいろとトラブルシュートしている途中で、自宅WifiにつながっているPCからhttp://<ルーターのグローバルIP> に行ってみたところ、Wifiルーターの管理コンソールへのログイン画面が出てきました。あれ、実はうちのWifiルーターは、これまでずっとインターネットから管理画面にアクセス可能だったのでしょうか!?

これはまずいです。あわてて対処しないと! とりあえず容易に想像できないパスワードをつけていてよかったですが。。。

と、ここでスマホでWifiを切ってルーターのグローバルIPにHTTP接続してみたところ、無事に(?)ルーターの管理コンソール画面は出てきませんでした。どうやら、PCからアクセスしたのがWifiからだったからのようでした。かなり焦りました。

4. さいごに

今回はインターネット経由で自宅サーバー上のImmichをセキュアにアクセスできるようにしてみました。今後の課題がいくつかあるので、解決したら記事をアップデートしたいと思います。

課題:

  • Immich専用の証明書になっている。他のサービスを追加することはできるのか?
  • CaddyはIPアドレスを指定してImmichとやり取りをしている(気がする)。別コンテナにいるサービス間でIPを使わずに通信できないのか?

Tech Tech