宅内WebサービスをCaddyとPocket IDでまるごとPasskey認証に
Synologyを辞めてProxmox VE+Rockstorに移行してからというもの、LXCコンテナでOSSのWebサービスを気軽に立てては試す日々が続いている。Proxmox VE Helper-ScriptsとそれをWeb UIから実行できるPVE Scripts Localのおかげで、数クリックでLXCコンテナの構築とサービスのデプロイができてしまい、気になるサービスがあればとりあえず立てまくっている。paperless-ngxで書類管理、immichで写真管理、calibre-webで電子書籍管理と、気がつけばサービスが数十個に膨れ上がっていた。
しかし、サービスが増えるにつれて困りごとも増えてきた。お試しで使う-うちは 192.168.10.15:8083 のようなアドレスを直打ちしていたが、ちゃんと運用するならドメイン割り当て・証明書管理・リバースプロキシ設定を数十個分やらなければならない。愚直に一個一個設定していったらまあ投げ出したくなる。そこでCaddyのダイナミックアップストリームとワイルドカード証明書の自動取得を組み合わせて、サービスを立てるだけでほぼ何もせずにドメイン設定とHTTPS化が全部反映されている状態を作った。
次なる困りごとは認証だ。サービスごとにバラバラなログインが面倒で、投げやりに認証なしで運用してしたりしていた。LDAPで統合する手もあるが、宅内のことに大袈裟な仕組みを導入する体力はもうない。そこで見つけたのがPocket ID。Passkey認証に対応した至極シンプルなOIDCプロバイダーだ。これとCaddyを組み合わせることで、サービスが立ったらほんの少しの設定でPasskey認証からログイン画面のスキップまで全部付いてくる状態を作れたので、その記録をここに残しておく。
目次
やったこと
サービスを増やすたびに発生していた手作業を、2つの仕組みを組み合わせることで解消した。
- Caddy + Proxmox VE SDN: IP直打ち・証明書管理・個別プロキシ設定 → ダイナミックアップストリームとワイルドカード証明書でサービスを立てたらHTTPSアクセスが自動で整う
- Pocket ID + Caddy: バラバラな認証 → OIDCゲートウェイでPasskey認証を一括適用して各サービスのログイン画面もスキップ
なお、今回は一人利用を前提としており、サービス追加時にこれらを一切の個別設定なしに解決する目的のため、複数ユーザーへの対応(アクセス制限や権限設定)は扱わない。
ネットワーク構成
Proxmox VEで構築しているネットワーク構成の全体像を以下に示す。
役割と経路
登場人物が多いので、それぞれの役割を整理しておく。
| コンポーネント | ネットワーク | 役割 |
|---|---|---|
| OpenWRT (VM) | LAN 192.168.1.1 | ルーター・DHCP・DNS |
| Caddy (LXC) | LAN 192.168.10.2 + SDN (DHCP) | リバースプロキシ・OIDC認証 |
| PowerDNS (LXC) | SDN 10.0.100.2 | SDN向けDNSサーバー |
| Pocket ID (LXC) | SDN (DHCP) | OIDCプロバイダー |
| 各種サービス (LXC) | SDN (DHCP) or LAN | grafana, paperless-ngx, immich, calibre-web等 |
ネットワークは大きく2つに分かれている。
- LAN (
192.168.0.0/16): Proxmox VEのvmbr0でブリッジされた通常のネットワーク。PCやNAS、OpenWRTがいる - SDN (
10.0.100.0/24): Proxmox VEのSDN機能で構築したvnet0。Webサービス用のLXCコンテナを収容する
SDNではDHCPで接続されたコンテナに10.0.100.10以降のアドレスを割り振っている。設定でSNATを有効にしてあるので、vnet0に繋がったコンテナからも外部への通信は可能だ。SDNファイアウォールも定義してあり、ファイアウォールを有効にすればFORWARDをDROPして閉じ込めることもできるようにしてある。
Helper-Scriptsでvnet0をデフォルトにするTip💡
Tip
Helper-Scriptsで作るコンテナが自動でvnet0に繋ぐよう、 /usr/local/community-scripts/default.vars にデフォルト値設定 var_brg=vnet0 を記述している。PVE Scripts LocalのWeb UIからのインストールではデフォルト値が反映されないバグがあるので、Advancedインストールでvnet0を指定している。
SDNに接続されたLXCコンテナには <ホスト名>.int.mzyy94.com の形式で自動的にDNSレコードが割り当てられる。これはProxmox VE SDNの PowerDNS plugin によるもので、コンテナの作成に連動してPowerDNSのAPIを叩き、Aレコードを自動登録してくれる。
SDNの構築やPowerDNS pluginの設定については、以下が参考になる。
- Proxmox VE SDN 公式ドキュメント - Software-Defined Network
- Proxmox VEでVM作成時にDNSに自動登録 - Qiita
- Proxmox VE 8.1で標準搭載になったSDNを試す - INTERNET Watch
LAN構成はOpenWRTをルーターとしていて、設定で*.int.mzyy94.com の名前解決先をCaddyの192.168.10.2に向けている。また、ローカルドメインをlan.mzyy94.comとしているので、DHCPで割り振った先のホスト名を加えた <ホスト名>.lan.mzyy94.com でOpenWRTに問い合わせると、割り当てたIPアドレスが返ってくるようにしてある。

PowerDNSのインストール自体は公式のインストール手順に従い、HTTP APIを有効にしてProxmox VE SDNからアクセスできるようにしておく。Hepler-ScriptsでdebianのLXCを作ってそこにaptで導入した。
Caddyはvmbr0とvnet0の両方に接続されていて、LANとSDNの橋渡し役を担う。LANからの *.int.mzyy94.com へのリクエストを受け取り、SDN上(またはLAN上)のサービスにリバースプロキシする。
Pocket IDの説明はまた後で。
HTTPSプロキシの流れ
このネットワーク構成でLAN上のPCから https://paperless-ngx.int.mzyy94.com にアクセスした場合のHTTPSプロキシの流れを図にする。
PCからブラウザでアクセスすると、①DHCPで渡された192.168.1.1 (OpenWRT) にDNSを問い合わせ、②*.int.mzyy94.comはCaddyの192.168.10.2に解決される。③PCはCaddyにHTTPSで接続し、④CaddyがPowerDNSに問い合わせてSDN上のpaperless-ngxのアドレスを得て、⑤HTTP経由でプロキシする。
①と②はOpenWRTがやってるので、③以降をCaddyの設定ファイル「Caddyfile」を記述することで実現する。
Caddyによるリバースプロキシ
Caddyで *.int.mzyy94.com の動的リバースプロキシを構成する。WebAuthnにはHTTPSが必須のため、Pocket IDの導入は後にして、先にHTTPSプロキシの仕組みを整える。
Caddyfileの公式ドキュメントは網羅的ではあるものの、実際の設定例が少なく読み解きにくい。よく訓練したAIでさえ読み違えるくらいなので、この記事ではCaddyfileの各ブロックを実例とともに解説していく。
Caddyの導入はHelper-Scriptsを使ってもらうとして、導入後にcaddy-dns/cloudflareプラグインを追加しておく。CloudflareのDNS APIを使ってワイルドカード証明書を取得するためだ。他の方法で証明書を取得する人は、適切なものに読み替えてほしい。
caddy add-package github.com/caddy-dns/cloudflare
ワイルドカード証明書の取得
*.int.mzyy94.com のワイルドカード証明書は、CloudflareのDNS APIを使ったDNS-01チャレンジで取得している。ワイルドカード欲しがりな人にはお馴染みの方法なので、他の人の記事を参考にAPIトークンを取得し、/etc/caddy/.env に CF_API_TOKEN=your-cloudflare-token の行を追記しておく。
- ワイルドカード付きSSL証明書の自動発行には Cloudflare が便利 | Zenn
- cloudflare DNSとcertbotでローカル用のワイルドカード証明書を取得する(自分向け完全版) #TLS証明書 - Qiita
- CloudflareのDNSを利用しているドメインの証明書をcertbotで発行する - akky blog
*.int.mzyy94.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
resolvers 1.1.1.1
}
# バックエンド転送ルートを指定(後述)
invoke dynamic-backend
}
resolversにCloudflareの1.1.1.1を指定しているのは、DNSチャレンジのTXTレコードの伝播確認をCloudflare自身に問い合わせるため。CaddyコンテナにはSDNも繋がっており、DHCPのタイミングで /etc/resolv.conf が上書きされてしまうため、明示的に指定している。
なお、以前のバージョンのCaddyとcaddy-dns/cloudflareにはスプリットホライズンDNS環境で証明書の取得に失敗する問題があり、証明書の発行がうまくいかなかった。別の方法で取得したものを用いて回避していたが、2026年2月23日にリリースされたCaddy v2.11.1で修正され利用できるようになっていた。
動的バックエンドの名前解決
最近のモダンなリバースプロキシではお馴染みの動的バックエンド解決。Caddyではdynamic upstreamsという機能として用意されている。
Caddyfileではnamed routeとしてdynamic-backendを定義し、先ほどの転送ルート指定部分でinvokeで呼び出している。わざわざ定義して呼び出す手順を踏んでいるのは、続くOIDC認証をする場合にこの経路を複数箇所で再利用するためだ。
&(dynamic-backend) {
reverse_proxy {
dynamic multi {
a {
name {host}
port 80
resolvers 10.0.100.2 # PowerDNS
}
a {
name {labels.3}.lan.mzyy94.com
port 80
resolvers 192.168.1.1 # OpenWRT
}
}
}
}
dynamic multiは複数のダイナミックアップストリームモジュールの結果を結合してアップストリームプールを構成するモジュールだ。すべてのソースに問い合わせ、解決できた結果をまとめてリストにする。この構成では各ホスト名はどちらか一方のDNSにしか存在しないので、結果的に1つだけが解決される。
- PowerDNS(
10.0.100.2)に問い合わせる:{host}、つまりpaperless-ngx.int.mzyy94.comをそのままAレコードとして引く。SDN上のコンテナならここで解決する - OpenWRT(
192.168.1.1)に問い合わせる:{labels.3}.lan.mzyy94.comに変換して引く。{labels.3}はホスト名の3番目のラベル、つまりpaperless-ngx.int.mzyy94.comならpaperless-ngx部分。これをpaperless-ngx.lan.mzyy94.comとして、OpenWRTのDHCPで管理されるLAN上のホストを探す
SDN上のコンテナならPowerDNSだけが解決し、LAN上のホストならOpenWRTだけが解決するので、結果的にどちらか一方のアドレスがアップストリームとして使われる。先ほどの図でPowerDNSへの ④ DNS lookup の解決に失敗していた場合、OpenWRTに問い合わせたpaperless-ngx.lan.mzyy94.comのAレコードががあればそこをアップストリームとするのだ。
この仕組みにより、SDN上のコンテナもLAN上のコンテナも、同じ <ホスト名>.int.mzyy94.com のドメインで統一的にアクセスできる。新しいサービスを立てたら、SDNでもLANでもホスト名をサブドメインにアクセスするだけで、自動でリバースプロキシされていく。
80番ポート以外のサービスへの対応
dynamic-backendはポート80に対してリバースプロキシをかけている。3000番ポートなど80以外で動くサービスには、コンテナ内のnftablesでポートリダイレクトを設定する。
nft add table nat
nft add chain nat prerouting { type nat hook prerouting priority -100 \; }
nft add rule nat prerouting tcp dport 80 redirect to :3000
nft list ruleset | tee /etc/nftables.conf
systemctl enable nftables
これで外からの80番ポートへのアクセスが3000番に転送され、Caddyのdynamic-backendで疎通できる。この部分が唯一の手動で調整が必要な箇所で、冒頭で「ほぼ何もせず」と言っていた”ほぼ”が、このnftablesの設定にあたる。
nft-redirect.shを作って置いてあるので、80番ポート以外で動いているサービスに限り、Proxmox VEのシェルに入ってnft-redirect.sh 102 3000と打つ作業だけ手を動かすことになる。
Tip
LANで立てているコンテナでは、PCなどから直接そのコンテナに対してアクセスできる状態になっている。 そのままでは認証をCaddyでまとめても回避されてしまうので、nftablesのポートリダイレクト設定に加えて、filterでコンテナのサービスポートへの直接アクセスをブロックし、Caddy経由のみに制限しておくとよい。
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0 \; policy accept \; }
nft add rule inet filter input tcp dport { 80, 3000 } ip saddr 192.168.10.2 accept
nft add rule inet filter input tcp dport { 80, 3000 } dropこれで *.int.mzyy94.com でLAN・SDN上の各サービスにHTTPSアクセスできる状態が整った。ここまでのCaddyfileは次のようになっている。
&(dynamic-backend) {
reverse_proxy {
dynamic multi {
a {
name {host}
port 80
resolvers 10.0.100.2 # PowerDNS
}
a {
name {labels.3}.lan.mzyy94.com
port 80
resolvers 192.168.1.1 # OpenWRT
}
}
}
}
*.int.mzyy94.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
resolvers 1.1.1.1
}
invoke dynamic-backend
}
Pocket IDの導入
Pocket IDは、Go製の軽量なOIDC (OpenID Connect) プロバイダーだ。特徴的なのはWebAuthn/Passkeyをネイティブにサポートしており、その認証に特化していること。パスワードや他の方法は用意されておらず、Face IDやセキュリティキーでのみログインできる。

自宅サーバー勢の認証基盤としてはAutheliaやAuthentikが人気だが、今回はPocket ID + Caddyの組み合わせを選んだ。Autheliaはリバースプロキシの前段に認証ポータルを置くオールインワン型で、LDAP・TOTP・OIDCプロバイダーまで一通り揃っている。機能は豊富だがその分設定項目も多いため、構築・運用に労力を必要とする。Authentikはもっとてんこ盛り。対してPocket IDは「OIDCプロバイダーに徹する」と割り切っていて、やることがWebAuthn認証とOIDCクライアントのトークン発行だけ。管理画面でできる事は、ユーザ・グループの追加編集とクライアントの管理くらいだ。認証のゲートウェイ機能すら持たないため、そこをCaddyに任せることでそれぞれの役割を分離できる。設定も少ないため、運用も楽だと見立てた。
Pocket IDはSDNのvnet0上にLXCコンテナとして配置し、DHCPでアドレスを取得している。ホスト名はpocket-idにしたので、SDNのPowerDNS pluginにより pocket-id.int.mzyy94.com で名前解決できる。先ほど構築したCaddyのリバースプロキシにより、ブラウザからHTTPSでアクセスできる状態だ。
インストールはPocket IDのHelper-Scriptsで一発だ。
https://pocket-id.int.mzyy94.com/setup からPocket IDの管理者とPasskeyを作成したら、管理画面でOIDCクライアントを作成できるようになる。
Pocket IDのオリジンを変えてしまった場合の対処Tip💡
Tip
Pocket IDはWebAuthn(Passkey)を認証の軸にしているため、当たり前だがPasskeyの登録時のオリジン(ドメイン+プロトコル+ポート)が変わると既存のPasskeyが無効になる。
Helper-Scriptsのデフォルトのホスト名がpocketidだったため、最初はpocketid.int.mzyy94.comで立てていた。たくさん設定した後にホスト名が気になり pocket-id に変更したため、やらかしをしてしまった。
WebAuthn以外のログイン手段がないため詰んだと思ったが、CLIからワンタイムアクセストークンを発行すれば復旧できるとあった。.envがあるディレクトリ(例: /opt/pocket-id)で pocket-id one-time-access-token <ユーザー名> を実行し、一時的なログインコードで管理画面に入ってPasskeyを再登録した。
immichのPocket ID認証設定
Pocket IDがうまく動くことをまずimmichで確認しておく。immichのドキュメント OAuth Authentication | Immich にOAuthをどう設定すればいいかが書かれている。
OIDCクライアントの作成
Pocket IDの場合は管理画面で「OIDC Clients」から新規作成し、ドキュメントに記載の値を参考に、以下のように設定する。
- Name:
immich - Callback URLs:
https://immich.int.mzyy94.com/auth/loginhttps://immich.int.mzyy94.com/user-settingsapp.immich:///oauth-callback

作成後の Client ID と クライアントシークレット はImmich側の設定で使うので控えておく。
忘れがちなのが、下の方にある「許可されたユーザーグループ」の設定で、ログイン可能なユーザーグループをチェックして保存するか、制限なしにしておかなければならない。これを忘れると、ログイン完了してもDenyされてしまうので注意。
immichからPocket IDへの接続
こちらもドキュメント通りで、設定ページの認証設定からOAuthを開いて設定値を入れていく。CLIENT_ID と CLIENT_SECRET はそれぞれ Client ID と クライアントシークレットをコピペすればOK。ISSUER_URLはPocket ID管理画面の「詳細を表示」から OIDC Discovery URLの項目をコピペするか、https://<Pocket IDのドメイン>/.well-known/openid-configuration を手入力する。他はドキュメントに規定値が書かれてるので、その通り埋めていけば完了。

あとはアカウント設定からOAuth→OAuthへリンクするとPocket IDの認証画面に移動するので、Face IDなりセキュリティキーなりでログインする。ログインが成功したら今後はID/Passwordを使わずPasskeyでログインできるようになっている。

CaddyにOIDC認証を追加する
Pocket IDが動いていることを確認できたところで、Caddyに認証機能を組み込んでいく。
先にCaddyでどのように認証フローが行われるか全体像を図示する。
prometheus.int.mzyy94.comにアクセスした場合を例にとると、①PCからPrometheusにアクセスしたら、②Caddyが未認証を検知して302でPocket IDのログイン画面にリダイレクトする。③Pocket IDにPasskeyでログインするとCaddyが裏で認可コードをトークンに交換し、④セッションCookieを発行してから元のサービスに案内する。⑤以降はCookieが有効な間、それをCaddyが確認してバックエンドに直接プロキシされる。
この認証認可を行うのがcaddy-security。先ほどと同様にCaddyにプラグインを追加する。
caddy add-package github.com/greenpau/caddy-security
プラグインが追加できたら、この流れを実現する設定を順にしていく。
Caddy用OIDCクライアントの作成
Pocket IDでCaddyが前段で認証するためのOIDCクライアントを作成する。基本の手順はimmichと同じだが、Callback URLsが異なる。
- Name:
Caddy(“宅内サービス群”とかでもいい) - Callback URLs:
https://*.int.mzyy94.com/caddy-security/oauth2/generic/authorization-code-callback
Callback URLsにワイルドカードを指定しているのがポイント。caddy-securityは認証後に元のサービスのURLにコールバックを戻すため、*.int.mzyy94.com配下のすべてのサービスで同じOIDCクライアントを使うようにするためだ。ここを空欄にすると、最初にログインしたサービスのが入ってしまい、別のサービスで認証が弾かれてしまう。
caddy-securityでOIDC認証をかける
caddy-securityプラグインでPocket IDとの連携を設定する。 Pocket ID公式のCaddyガイドにCaddyfileの設定例が書かれているので拝借すればいいのだが、少し手を加えてある。Caddyfileの先頭に書くグローバルブロックに以下を記述する。
{
order authenticate before respond
security {
oauth identity provider generic {
delay_start 3
realm generic
driver generic
client_id {env.OIDC_CLIENT_ID}
client_secret {env.OIDC_CLIENT_SECRET}
scopes openid email profile
base_auth_url https://pocket-id.int.mzyy94.com
metadata_url https://pocket-id.int.mzyy94.com/.well-known/openid-configuration
extract all from userinfo
}
authentication portal pocket_id {
crypto default token lifetime 28800
enable identity provider generic
cookie insecure off
transform user {
match realm generic
action add role user
}
}
authorization policy pocket_id {
set auth url /caddy-security/oauth2/generic
allow roles user
inject headers with claims
inject header "Remote-User" from "userinfo|preferred_username"
}
}
}
各ブロックを上から順に見ていく。
oauth identity provider generic
Pocket IDをOIDCプロバイダーとして登録する。metadata_urlにOpenID Connectのディスカバリエンドポイントを指定すれば、認可エンドポイントやトークンエンドポイントは自動で取得される。delay_start 3はCaddy自身でpocket-id.int.mzyy94.comにリバースプロキシしているのを待つのに必須で、消すと起動にコケてしまう。extract all from userinfoは後述のヘッダー注入でpreferred_username等を参照するために必要な設定で、これがないとuserinfoのクレームを取り出せない。忘れるとRemote-Userヘッダーが空になり、リバースプロキシ認証が機能しない。
authentication portal pocket_id
認証ポータルの設定。トークンの有効期間は28800秒(8時間)にしてある。Passkeyなら毎回ログインする煩わしさも少ないため、寝て起きたらセッションが切れるくらいにしている。
authorization policy pocket_id
認可ポリシー。認証済みユーザーにuserロールを付与し、そのロールを持つユーザーにアクセスを許可する。inject header "Remote-User" from "userinfo|preferred_username"は、認証済みユーザーのユーザーIDをRemote-Userヘッダーにセット(上書き)してバックエンドに転送するための設定。後述するリバースプロキシ認証で利用する。
Client ID と クライアントシークレット は環境変数経由で引っ張ってくるので、/etc/caddy/.envにそれぞれ追記しておく。
ルーティングと認証の振り分け
認証認可の設定が済んだら *.int.mzyy94.com サイトブロック内で、リクエストに応じて認証の要否を振り分けている。
*.int.mzyy94.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
resolvers 1.1.1.1
}
@noauth {
host pocket-id.int.mzyy94.com
host immich.int.mzyy94.com
}
handle @noauth {
invoke dynamic-backend
}
@bypass-api {
path /api/*
header Authorization Bearer*
}
handle @bypass-api {
invoke dynamic-backend
}
handle {
handle /caddy-security/* {
authenticate with pocket_id
}
route {
authorize with pocket_id
invoke dynamic-backend
}
}
}
matchersとhandleの組み合わせで、3つのパターンに分岐している。
@noauth
認証をスキップするサイトを指定する。Pocket IDは認証プロバイダー自体なのでスキップしておかないと、Pocket IDの認証のためにPocket IDにリダイレクトし続けるループが起こる。
immichは先ほど設定したOIDC認証を自身で行うため、Caddy側で二重に認証をかける必要がないのでスキップしている。実際の挙動は二重で認証リクエストされても、Caddy側の認証を突破していれば連続する2つ目のログイン画面は自動承認してくれるため、スキップしなくても体験としてはあまり差がない。Pocket IDで「再認証が必要」を設定している場合は自動承認されないため、スキップしておくべきだろう。 他にCaddyでの認証をスキップしたいホストは、同様に続けてホスト名を列挙していく。
@bypass-api
/api/*パスかつAuthorization: Bearer ...ヘッダーを持つリクエストは認証をバイパスする。ブラウザを介さないモバイルアプリなど、APIトークンで認証するクライアント向け。Bearer以外の認証スキームにも対応したければ、続けてheader Authorization Token*などと列挙していけば同様にバイパスされる。X-Api-Keyなど別のヘッダー名もバイパスさせたいとなると列挙しては書けないので、ブロック全体をCELに置き換える必要がある。
@bypass-api `path('/api/*') && (header({'Authorization':'Bearer *'}) || header({'X-Api-Key':'*'}))`
それ以外
routeブロック内でcaddy-securityのauthorizeを通し、Pocket IDでのOIDC認証を要求する。未認証のユーザーはPocket IDのログイン画面にリダイレクトされ、Passkeyで認証する。
Caddyfile全体
ここまでの全てを設定したCaddy v2.11.1で実際に動くCaddyfileをgistに貼っておいた。半年たらずだがトラブルなく安定して動いており、満足いく構成を作ることができた。
リバースプロキシ認証
CaddyでOIDC認証を通過しても、サービス自体がログイン画面を持っているとIDとパスワードを求められてしまう。これを解消するのがリバースプロキシ認証だ。
Caddyが認証済みユーザー名をRemote-Userヘッダーとしてバックエンドに転送すると、サービスはこのヘッダーを信頼してログインをスキップし、そのユーザーでログイン済みとする。各サービスにアクセスできるのをCaddyだけに構成しているため、Caddyが唯一ヘッダーを付与する認証ゲートウェイとして機能するからこそ実現できる技だ。
多くのサービスがこの認証方法に対応しているため、いくつか取り上げて設定について記録しておく。
calibre-web
calibre-webは独自のログイン画面を持つが、リバースプロキシ認証に対応している。特定のHTTPヘッダーにユーザー名が含まれていれば、そのユーザーとして自動ログインする仕組みだ。

これを有効にするため、calibre-webの基本設定の機能設定で「リバースプロキシの認証を許可」を有効にし、リバースプロキシのヘッダー名にRemote-Userを指定する。
前述のCaddyfileの認可ポリシーで、inject header "Remote-User" from "userinfo|preferred_username" と設定しているため、Pocket IDで認証したユーザーのpreferred_username、すなわちユーザーIDがRemote-Userヘッダーとしてcalibre-webに渡される。そして同じユーザーIDがcalibre-webにあれば、ログイン画面を飛ばしてログイン済みになる。
Grafana
リバースプロキシ認証
多彩な認証方法が提供されているGrafanaは、auth proxy項目としてリバースプロキシ認証を有効にできる。grafana.iniで次の通り設定するだけだ。
[auth.proxy]
enabled = true
header_name = REMOTE-USER
代替: Generic OAuth
リバースプロキシ認証の代わりに、Grafana自身のOIDC機能でPocket IDと直接連携することもできる。Webの設定画面からGeneric OAuth設定を探し、Pocket IDの対応する値をコピペしていくだけで設定できる。

さらにPocket ID側で管理しているグループをGrafanaのロールにマッピングすることもできる。Pocket IDでadminグループを作って管理者にしたいユーザーをそこに追加しておき、Grafanaでgroupも得られるようにScopesに openid email profile group を設定。折りたたまれているUser mappingを展開してRole attribute pathの入力欄に contains(groups[*], 'admin') && 'Admin' || 'Viewer' と設定することで、サインイン時にグループを確認してadminにも所属していればAdminロールを持ったアカウントとして扱われるようになる。

ちなみにauth proxyの方でもheaders = Name:X-WEBAUTH-NAME Role:X-WEBAUTH-ROLE Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPSなどと設定を追加することで、ヘッダーに書かれたロールを直に反映冴えることができる。
paperless-ngx
リバースプロキシ認証
paperless-ngxもリバースプロキシ認証に対応している。paperless.confでリバースプロキシ下で動くようにする設定値と、リバースプロキシ認証用の設定値 の二つを次のようにpaperless.confに記載しておく。
PAPERLESS_URL=https://paperless-ngx.int.mzyy94.com
PAPERLESS_ENABLE_HTTP_REMOTE_USER=true
この状態で起動してあげることで、Remote-Userヘッダーを見て同じユーザーIDでログインしたことになる。
代替: Django allauth
リバースプロキシ認証の代わりに、paperless-ngx自身でOIDC認証を行うこともできる。今のCaddyによるリバースプロキシ認証をする前はこの方法でやっていた。
Djangoベースなので、Django AppsとしてallauthのOpenID Connectプロバイダーを追加できる。
PAPERLESS_ENABLE_HTTP_REMOTE_USERをコメントアウトして、以下の値を設定ファイルに記入して再起動することで、Pocket IDをOIDCとして登録可能になる。
PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect
起動して管理者でログインしたら、Djangoの管理画面からSocial Applications(/admin/socialaccount/socialapp/)のページに移動し、Pocket IDのOIDCクライアント情報を登録する。

設定項目はこれまで設定した他のOIDCクライアントと同様だが、OIDC Discovery URLの設定場所だけ Settings: の中でJSONとして記述するようになっている。
{"server_url": "https://pocket-id.int.mzyy94.com/.well-known/openid-configuration"}
ただ、Paperless-ngxはGrafanaのようにロールの割り当てができないため、Pocket IDでサインアップすると非管理者としてアカウントが作成されてしまう。非管理者ではドキュメント管理ができないので、adminでログインし直して「スーパーユーザー権限」を付与してあげる必要がある。先にDjangoからスーパーユーザーでアカウントを作成した後、そのアカウントでパスワードログインしてPocket IDに連携する方法がおすすめ。いや、リバースプロキシ認証の方がもっとおすすめ。
まとめ
色々なめんどくささを抱えていた宅内Webサービスが、CaddyとPocket IDで個別設定なしに便利に安全に使えるようになった。それぞれのサービスに個別に認証設定をして回る作業から解放されたいま、まるで春のような清々しさがある。Face IDやTouch IDでシームレスにで全てのサービスにアクセスできる体験はとても気持ちいい。
試してみて、Pocket ID。そしてCaddy