SquidでCertbotの認証用の通信を転送する、なるべくセキュアに


## 経緯


自宅サーバ上でNextcloudなどのwebアプリケーションを稼働させるにあたって悩ましいのが`https`をどうするか。
パブリックへは公開していないので盗聴などの危険性はあまりないとはいえ、`https`がスタンダードとなりゼロトラストが叫ばれる昨今、プライベートネットワークだからと例外扱いせず、SSL（TLS）でのエンドツーエンドな暗号化を行っておきたい。

公開サーバであれば[Let's Encypt](https://letsencrypt.org/ja/)（のクライアント用のコマンドである`certbot`）の`webroot`モードでかんたんにSSL証明書を作成・更新できるで問題はないのだが、自宅サーバだとそうは行かない。というのも`webroot`で証明書を取得するにはグローバルからの80ポートへのアクセスが必要になる。グローバルIPを持たない自宅サーバは当然そのままでは取得することはできないわけだ。[^oreore]

上記の事情によりこれまでは**ブログを公開している別のサーバ**で**プライベート用のドメインの証明書を作成**して、**自宅サーバにコピー**していたのだが、3か月に1回程度[^period]とはいえ手動でやるのは面倒だし、自動でやるにしても本来`root`からしかアクセスできない証明書ファイルを外部のホストから機械的に`sudo`で無理やりコピーするというのは行儀がよくない気がする。

ということで、パブリックにある中継用のサーバからリバースプロキシを経由して自宅サーバ自身が取得するやり方に変えることにした。[^nat]

## 目的
- グローバルIPを持たない自宅サーバで`certbot` `webroot`を用い、プライベート用のドメイン（例: `nextcloud.home.example.com`など、`home.example.com`配下のドメイン）を取得できるようにする。
- このためにグローバルIPを持つ中継サーバ上で`certbot`用のアクセス（80番ポートで、パスは`/.well-known/acme-challenge/*`）をリバースプロキシで転送して自宅サーバへ転送する。
- この際に`certbot`と関係ないアクセスはなるべく中継サーバ側で弾くようにする

## 構成

- 中継サーバ
  - OSは Debian
  - Lightsailで動かしているVPS
  - グローバルIPを持っている
  - [Wireguardの中継サーバとして利用している](https://qiita.com/fluo10/items/78e91884042645b08fb9)
- 自宅サーバ
  - OSは Manjaro
  - nginxを通してWebサービスを提供している
  - グローバルIPは持っていない
  - 自分の端末からはWireguardを用いて中継サーバを経由してアクセスできるようになっている。

### 例示用のIPアドレス

以下は例に使っているIPの一覧になる

ホスト名・インターフェース|IPアドレス
:--|:--
中継サーバのグローバルIP|`192.0.2.1`
自宅サーバのVPN用のIP|`10.0.0.2`
中継サーバのVPN向けのIP|`10.0.0.1`

## Squidの設定（@中継サーバ）

### インストール（なければ）

```
sudo apt install squid
```

```
$ sudo squid -v | head -n 1
Squid Cache: Version 4.6
```

### 設定ファイルの編集

（Debianの）squidの設定ファイルは`/etc/squid/squid.conf`が本体で、`/etc/squid/conf.d` 内のファイルを読み込むという形になっていた
そのため今回は以下のように2つのファイルに分けて設定した

- **自宅サーバへの転送用リバースプロキシ**としての設定を`/etc/squid/conf.d/certbot.conf`に
- 全体に関わる設定（セキュリティのためにバージョン情報とかを隠すなど）を`/etc/squid/squid.conf`に

#### squid.conf

デフォルトから変更した内容としては以下

- フォワードプロキシ用の設定を無効化
- エラー画面でのホスト情報をなるべく隠蔽する

```/etc/squid/squid.conf
# もともとコメントだった行は省略
# 以下で残っているコメントはデフォルトで有効だった設定
# 将来的にフォワードプロキシとしても使う可能性があるのでコメントアウトして残しておいた
#acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
#acl localnet src 10.0.0.0/8            # RFC 1918 local private network (LAN)
#acl localnet src 100.64.0.0/10         # RFC 6598 shared address space (CGN)
#acl localnet src 169.254.0.0/16        # RFC 3927 link-local (directly plugged) machines
#acl localnet src 172.16.0.0/12         # RFC 1918 local private network (LAN)
#acl localnet src 192.168.0.0/16                # RFC 1918 local private network (LAN)
#acl localnet src fc00::/7              # RFC 4193 local private network range
#acl localnet src fe80::/10             # RFC 4291 link-local (directly plugged) machines
#acl SSL_ports port 443
#acl Safe_ports port 80         # http
#acl Safe_ports port 21         # ftp
#acl Safe_ports port 443                # https
#acl Safe_ports port 70         # gopher
#acl Safe_ports port 210                # wais
#acl Safe_ports port 1025-65535 # unregistered ports
#acl Safe_ports port 280                # http-mgmt
#acl Safe_ports port 488                # gss-http
#acl Safe_ports port 591                # filemaker
#acl Safe_ports port 777                # multiling http
#acl CONNECT method CONNECT
#http_access deny !Safe_ports
#http_access deny CONNECT !SSL_ports
#http_access allow localhost manager
#http_access deny manager

# 読み込む設定ファイル群
# 今回はCertbotへの転送はcertbot固有の処理でグローバルに書くのは適さないと思ったので
include /etc/squid/conf.d/*
#http_access allow localhost

# /etc/squid/conf.d/*で許可されていないアクセスはすべて削除
http_access deny all
#http_port 3128
coredump_dir /var/spool/squid # これはコメントアウトしてもデフォルト値が適用されるらしいのでそのままにしておいた

# Squidのエラーが面でホスト名やバージョンが表示されるので、隠す
visible_hostname unknown
httpd_suppress_version_string on

# Squidがdeny all した場合にはエラー自体表示させないようセッションを切るように設定する
# nginx の return 444 に相当
deny_info TCP_RESET all

# キャッシュ関係、使わないのでコメントアウト
#refresh_pattern ^ftp:          1440    20%     10080
#refresh_pattern ^gopher:       1440    0%      1440
#refresh_pattern -i (/cgi-bin/|\?) 0    0%      0
#refresh_pattern .              0       20%     4320
```

#### certbot.conf

リバースプロキシ用の設定

```/etc/squid/conf.d/certbot.conf
http_port 80 accel vhost # 80番ポートをリバースプロキシモードで開ける。バーチャルホストも有効にする

# アクセス条件の設定
acl http_port port 80 # 現状だと80番あてのパケットしか届かないのでいらない気もするが念のため
acl home_domain dstdom_regex ^.*home\.example\.com$ # 正規表現で自宅に転送するドメインを制限する
acl certbot_path urlpath_regex ^/\.well-known/acme-challenge/.*$ # 正規表現で自宅に転送するパスをcertbot用のに制限
http_access allow http_port home_domain certbot_path # 上の三つのaclに一致するアクセスのみ受け取る

# `home_peer`という名前で転送先を設定
cache_peer 10.0.0.2 parent 80 0 no-query originserver name=home_peer

# `home_peer`に転送するパケットの条件を設定
#（今回は上の転送対象のパケット以外は受け取らないので、allow allでも問題はないと思う。）
cache_peer_access home_peer allow http_port home_domain certbot_path
cache_peer_access home_peer deny all
```

### nginx@自宅サーバ

nginxの設定。といっても通常の`certbot`用の設定と変わらないのだが、一応参考までに載せておく。
nginxはすでに稼働中なのでインストールや全体の設定は省略。

```
$ sudo nginx -v                                                               
nginx version: nginx/1.20.0
```

#### `server`の設定例

- サイト別の設定を`/etc/nginx/sites-enabled/*`から読み込んでいる[^available]ので、今回の設定も各サイト別に行う。
- 80番ポートはletsencrypt以外はすべて転送する≒ドメイン別にする必要がないため、rootのパスはドメインに関係なく`/var/lib/letsencrypt/`を使う

```/etc/nginx/sites-enabled/nextcloud
server {

    listen 80 ;
    listen [::]:80 ;
        server_name nextcloud.home.example.com;

        #中略
        
        root /var/lib/letsencrypt/;
        location ^~ /.well-known/acme-challenge {
                allow all;
        default_type "text/plain";
        try_files $uri =404;
    }
        location / {
                return         301 https://$host$request_uri;
    }

}

# 中略

```

これで以下のようにCertbotを実行すると無事更新できた。

```
sudo certbot certonly --webroot -d nextcloud.home.example.com -w /var/lib/letsencrypt
```

## 検証

狙い通りに不要なパスへのアクセスが制限されているかどうか確認する。

### ダミーファイルの準備

適当にダミー用のファイルを作成

```
echo TEST | sudo tee /var/lib/letsencrypt/.well-known/acme-challenge/dummy.txt
```

### 有効なパス、ドメイン

```
curl  --resolv nextcloud.home.example.com:80:192.0.2.1   http://nextcloud.home.example.com/.well-known/acme-challenge/dummy.txt
TEST
```

無事取得できる

### 有効だがファイルが存在しない

```
curl  --resolv nextcloud.home.example.com:80:192.0.2.1   http://nextcloud.home.example.com/.well-known/acme-challenge/index.html
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
```

### 無効なパス

```
curl  --resolv nextcloud.home.example.com:80:192.0.2.1   http://nextcloud.home.example.com/                                    
curl: (56) Recv failure: 接続が相手からリセットされました
```

### 無効なドメイン

```
curl  --resolv invalid.example.com:80:192.0.2.1  http://invalid.example.net/.well-known/acme-challenge/dummy.txt 
curl: (56) Recv failure: 接続が相手からリセットされました
```

### squid上で有効だがnginxで無効なドメイン

サブドメインを増やすたびにsquidを設定するのが面倒なので、実際にサービスに割り当てられてなくてもサブドメインがhome.example.comなら転送するようにした。
そのためsquidでは有効なので転送するが自宅サーバのnginxではねられるケースが発生するので、それを試す。

```
$ curl  --resolv invalid.home.example.com:80:192.0.2.1   http://invalid.home.example.com/.well-known/acme-challenge/dummy.txt
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>

<!-- 中略 -->

</head><body id=ERR_ZERO_SIZE_OBJECT>
<div id="titles">
<h1>ERROR</h1>
<h2>The requested URL could not be retrieved</h2>
</div>

<!-- 中略-->

<p>Squid did not receive any data for this request.</p>

<!-- 中略-->

</body></html>

```

squidからnginxへのアクセスではねられた場合はエラー画面が表示される模様。
まあsquidは正しく転送している以上仕方がない。 

## その他

今回は設定していないが、squidの転送の条件には時間も指定することができる模様。
certbotは1日2回実行するのが推奨されているので、その時間の前後だけ転送を許可するようにすれば強固にできそうではある。
様子をみて、certbot以外のアクセスがそれなりにあるなら試すかもしれない。


## 参考ページ

- [Squidを使ってReverseProxyを構築してみた](https://blog.trippyboy.com/2011/squid2-6/squid2-6-reverseproxy%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/)
- [Squid の設定と使い方 - Qiita](https://qiita.com/ekzemplaro/items/3b0e6e29d6673ce96281)
- [Apache＆Squidアクセス制御／ユーザー認証の設定：実践でも役立つLPICドリル（11）（2/4 ページ） - ＠IT](https://www.atmarkit.co.jp/ait/articles/0904/08/news120_2.html)
- [deny_info](https://www.robata.org/docs/squid/Reference/squid3.4.0.3/deny_info.html)
- [Squid - ArchWiki](https://wiki.archlinux.jp/index.php/Squid)

[^oreore]: 自宅専用ならオレオレ証明書という手もあるが、これは設定が多すぎて難しい上にAndroidだけ動かなかったりで原因究明が面倒でやめた。下手にここに時間をさくよりはFWに最小限の穴をあけるリスクを取ってでも他のセキュリティ対策に力を入れたほうがいいと判断した。

[^period]: `Certbot`の証明書の有効期限が3ヶ月のため

[^nat]: プロキシではなくnftableなど、IP・TCPレベルでの転送をするという手もあったが、Certbotのアクセス元IPが公開されておらずIPでの制限ができない以上、少しでも無関係なアクセスは減らしたかったので、パスでフィルタリングできるプロキシを選んだ。

[^available]: 厳密には`sites-enabled`にあるのはソフトリンクで、`sites-available`内の原本を参照するようになっている。設定ファイルを残したままサイトの有効・無効を切り替えられるようこうなっているとか。