Type something to search...
宅内WebサービスをCaddyとPocket IDでまるごとPasskey認証に

宅内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認証からログイン画面のスキップまで全部付いてくる状態を作れたので、その記録をここに残しておく。

目次

Open 目次

  1. やったこと
  2. ネットワーク構成
    1. 役割と経路
    2. HTTPSプロキシの流れ
  3. Caddyによるリバースプロキシ
    1. ワイルドカード証明書の取得
    2. 動的バックエンドの名前解決
    3. 80番ポート以外のサービスへの対応
  4. Pocket IDの導入
    1. immichのPocket ID認証設定
      1. OIDCクライアントの作成
      2. immichからPocket IDへの接続
  5. CaddyにOIDC認証を追加する
    1. Caddy用OIDCクライアントの作成
    2. caddy-securityでOIDC認証をかける
      1. oauth identity provider generic
      2. authentication portal pocket_id
      3. authorization policy pocket_id
    3. ルーティングと認証の振り分け
      1. @noauth
      2. @bypass-api
      3. それ以外
    4. Caddyfile全体
  6. リバースプロキシ認証
    1. calibre-web
    2. Grafana
      1. リバースプロキシ認証
      2. 代替: Generic OAuth
    3. paperless-ngx
      1. リバースプロキシ認証
      2. 代替: Django allauth
  7. まとめ

やったこと

サービスを増やすたびに発生していた手作業を、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.2SDN向けDNSサーバー
Pocket ID (LXC)SDN (DHCP)OIDCプロバイダー
各種サービス (LXC)SDN (DHCP) or LANgrafana, 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の設定については、以下が参考になる。

LAN構成はOpenWRTをルーターとしていて、設定で*.int.mzyy94.com の名前解決先をCaddyの192.168.10.2に向けている。また、ローカルドメインをlan.mzyy94.comとしているので、DHCPで割り振った先のホスト名を加えた <ホスト名>.lan.mzyy94.com でOpenWRTに問い合わせると、割り当てたIPアドレスが返ってくるようにしてある。

OpenWRT DHCP setting

PowerDNSのインストール自体は公式のインストール手順に従い、HTTP APIを有効にしてProxmox VE SDNからアクセスできるようにしておく。Hepler-ScriptsでdebianのLXCを作ってそこにaptで導入した。

Caddyvmbr0vnet0の両方に接続されていて、LANとSDNの橋渡し役を担う。LANからの *.int.mzyy94.com へのリクエストを受け取り、SDN上(またはLAN上)のサービスにリバースプロキシする。

Pocket IDの説明はまた後で。

HTTPSプロキシの流れ

このネットワーク構成でLAN上のPCから https://paperless-ngx.int.mzyy94.com にアクセスした場合のHTTPSプロキシの流れを図にする。

DNS flow

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/.envCF_API_TOKEN=your-cloudflare-token の行を追記しておく。

*.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つだけが解決される。

  1. PowerDNS(10.0.100.2)に問い合わせる: {host}、つまりpaperless-ngx.int.mzyy94.comをそのままAレコードとして引く。SDN上のコンテナならここで解決する
  2. 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やセキュリティキーでのみログインできる。

pocket id client config

自宅サーバー勢の認証基盤としてはAutheliaAuthentikが人気だが、今回は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/login https://immich.int.mzyy94.com/user-settings app.immich:///oauth-callback

Immich OIDC client setting

作成後の Client IDクライアントシークレット はImmich側の設定で使うので控えておく。

忘れがちなのが、下の方にある「許可されたユーザーグループ」の設定で、ログイン可能なユーザーグループをチェックして保存するか、制限なしにしておかなければならない。これを忘れると、ログイン完了してもDenyされてしまうので注意。

immichからPocket IDへの接続

こちらもドキュメント通りで、設定ページの認証設定からOAuthを開いて設定値を入れていく。CLIENT_IDCLIENT_SECRET はそれぞれ Client IDクライアントシークレットをコピペすればOK。ISSUER_URLはPocket ID管理画面の「詳細を表示」から OIDC Discovery URLの項目をコピペするか、https://<Pocket IDのドメイン>/.well-known/openid-configuration を手入力する。他はドキュメントに規定値が書かれてるので、その通り埋めていけば完了。

Immich Pocket ID setting

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

Immich Passkey Auth OK

CaddyにOIDC認証を追加する

Pocket IDが動いていることを確認できたところで、Caddyに認証機能を組み込んでいく。

先にCaddyでどのように認証フローが行われるか全体像を図示する。

OIDC認証フロー

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
        }
    }
}

matchershandleの組み合わせで、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 reverse proxy auth

これを有効にするため、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の対応する値をコピペしていくだけで設定できる。

grafana generic oauth

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

grafana signup

ちなみに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クライアント情報を登録する。

paperless-ngx django admin page

設定項目はこれまで設定した他の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

Related Posts

debianでiscsi target/initiator動作させるメモ

Linux上でのiSCSIパケット収集のためにVirtual Boxで作成した、Debian 7.0.0にiSCSI initiatorとtargetを入れたときのメモを起こしておきます。 今回i ...

read more
1万円台の格安Intel X540-T2 10GbEを買ってみた

1万円台の格安Intel X540-T2 10GbEを買ってみた

事の発端はこちらのツイート Amazonに出品されてる激...

read more
一般のご家庭向けEAP-SIM認証Wi-Fi

一般のご家庭向けEAP-SIM認証Wi-Fi

ご家庭のWi-Fi、まだパスワード認証ですか? こんにちは。陽炎型航洋直接教育艦 晴風の艦長、岬明乃です。 昨日開催されたカーネル/VM探検隊 ...

read more
一般のご家庭に25GbEネットワークを導入する

一般のご家庭に25GbEネットワークを導入する

師走と言えば大掃除。 一般のご家庭に溜まる可燃ゴミの掃除はもちろんのこと、デジタルデータに溢れる令和の時代は、データ掃除も入念に行う必要があります。 PCやiPadやNASなど、いたるところに散らばる ...

read more
DIALというネットワークプロトコル

DIALというネットワークプロトコル

家庭のネットワークの監視システムからDIALなるプロトコルが暴れていてアラートが飛んできたので調査しました。 目次 日頃の監視と増えた謎のリクエスト 自宅のシステムを管理する上で、ネ ...

read more
中華10GbEスイッチNetcore GS6を運用して1ヶ月たったレビュー

中華10GbEスイッチNetcore GS6を運用して1ヶ月たったレビュー

春に新居に越してからベストエフォートで10Gbpsのインターネット回線を引き込み利用している。 リビングに光コンセントを設置し、ONUに自作ルーターにNASやら母艦やらを集中してリビングに設置していた ...

read more
NAS4FreeでTime Machineの設定するメモ

NAS4FreeでTime Machineの設定するメモ

Mavericksの登場で手元のMacBook Airをクリーンインストールしました。 その際、バックアップから復元するような手段は取らず、必要なデータのみを移して環境を再構築しました。 というのも、 ...

read more
自宅サーバーを増やした

自宅サーバーを増やした

増税前のPCパーツ買いだめイベントに便乗して、3月中に自宅サーバーを増設しました。 どんな構成で組み立てたのかの紹介になります。 目次 サーバー増設にあたって、次に挙げる構成目標を先に決め ...

read more
サーバーの発熱でこたつを温める

サーバーの発熱でこたつを温める

この記事はcoins Advent Calendarの一部です。 寒い日が続きますね。昨夜の外気温は摂氏2度でした ...

read more
Synologyへの不信感からRockstorに戻ってきた

Synologyへの不信感からRockstorに戻ってきた

ここ10年以上、NASを取っ替え引っ替えしてきた。 2010年代前半には自作NASが楽しく、FreeNASやNAS4FreeやOpenMediaVaultなど、OSSのものを色々と触って楽しんでいた ...

read more