From 40ca516249514228ebd1b2101381471e0697554e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 00:24:05 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=9FIP=E3=82=A2=E3=83=89=E3=83=AC?= =?UTF-8?q?=E3=82=B9=E5=8F=96=E5=BE=97=E3=81=A8=E3=83=A1=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E9=80=81=E4=BF=A1=E3=81=AE=E4=B8=8D=E5=85=B7=E5=90=88=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - service.externalTrafficPolicy: Local を追加し、kube-proxyのSNATによる 実IPアドレス消失を解消(ベアメタル/MetalLB構成向け) - wp-config.php内の危険な二重REMOTE_ADDR上書き処理を削除 (Nginx real_ipモジュールの信頼境界を迂回できるIP詐称の余地があった) - smtp.* 設定を追加し、msmtp経由の外部SMTPリレー送信に対応 (helmchart/phpfpm と同一の実装パターンを踏襲) - README.md / CLAUDE.md を更新 --- CLAUDE.md | 62 ++++++++++++++++++++++++++++++ Chart.yaml | 2 +- README.md | 61 ++++++++++++++++++++++++++++++ templates/configmap-smtp.yaml | 48 +++++++++++++++++++++++ templates/deployment.yaml | 71 ++++++++++++++++++++++++++++++----- templates/secret-smtp.yaml | 13 +++++++ templates/service.yaml | 5 +++ values.yaml | 49 +++++++++++++++++++++++- 8 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 CLAUDE.md create mode 100644 templates/configmap-smtp.yaml create mode 100644 templates/secret-smtp.yaml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..32a9ff0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md — WordPress Helm Chart + +## リポジトリ概要 + +Raspberry Pi などのベアメタルで稼働することを想定した、Kubernetes 上で動作する Alpine Nginx + WordPress (PHP-FPM) の Helm チャート。 + +- **Gitea リポジトリ**: `ssh://git@192.168.9.65/helmchart/wordpress` +- **Helm リポジトリ**: `https://git.cafepieters.com/api/packages/helmchart/helm` + +## 実行環境について + +WordPress本体はイメージ内蔵のもの(`docker.io/wordpress:-fpm-alpine`)をemptyDirにコピーして使い捨てで起動し、`wp-content`のみPVCで永続化する(bitnami方式)。PHPの実行環境はこのイメージに依存し、本リポジトリにPHPコードは含まれない。 + +## Git コミット情報 + +| 項目 | 値 | +|------|-----| +| 名前 | Claude | +| メール | claude@cafepieters.com | +| SSH キー | `P:\Claude\.ssh\id_claude` | + +## 作業完了のルール(重要) + +1. **機能を追加・変更した場合は、必ず README.md を更新すること。** 追加・変更した機能の説明を反映してから作業完了とする。 +2. **変更は必ず Git Commit / Push まで行うこと。** 作業単位ごとにコミットし、origin(main) へ push するまでが作業完了。 +3. **Commit メッセージに Claude クレジット(Co-Authored-By 等)を追記することは禁止。** `user.name = Claude` でコミットされるため、ユーザー名で判別可能。 +4. `git pull` で取り込まれた変更は必ず尊重し、revert して push することは禁止(既存の共通ルールと同じ)。 + +## チャート改修履歴 + +### 実IP取得の修正 + SMTP経由メール送信の追加(2026-08-08, v7.0.2-a) + +**背景**: ルーター(グローバルIP)配下の 192.168.9.x セグメントにコントロールプレーン/ワーカーノードがあり、その上のPodでWordPressが稼働する構成において、(1) 訪問者の実IPアドレスが正しく取得できない、(2) `wp_mail()` によるメール送信が失敗する、という2つの不具合が報告された。 + +**(1) 実IPアドレス取得の不具合** + +原因は2つ複合していた: +- Service(`LoadBalancer`/MetalLB想定)はデフォルトの `externalTrafficPolicy: Cluster` のため、kube-proxyが送信元IPをノード内部IPにSNATしてしまい、Nginxの `$remote_addr` が実IPではなくノードIPになっていた。 +- 本チャートの `nginx.forwardRealIP`(X-Forwarded-Forを信頼するreal_ipモジュール設定)は、Ingress ControllerやCDNなど**XFFヘッダーを付与するリバースプロキシが前段にある場合**にのみ機能する設計だが、このベアメタル構成ではService直下にPodがぶら下がるだけでXFFを付与する層が存在せず、機能していなかった(`trustedProxies` に `192.168.0.0/16` が含まれるためSNAT後のノードIPを「信頼済みプロキシ」とみなしてしまうが、そのIPがXFFを付与するわけではないので `$real_ip` は結局SNAT後のIPのままになる)。 + +さらに、`wp-config.php` 生成テンプレート内でPHP側が生のX-Forwarded-Forヘッダーを無条件に信頼してREMOTE_ADDRを上書きする処理があり、Nginxのreal_ip信頼境界を迂回してクライアントがIPを詐称できる状態だった(副次的なセキュリティ上の問題)。 + +**修正内容**: +- `values.yaml`: `service.externalTrafficPolicy: Local` をデフォルト追加(SNATを回避し実IPをそのまま透過させる。MetalLB L2モード等と組み合わせる想定。ClusterIPでは無視される)。 +- `templates/service.yaml`: `externalTrafficPolicy` を条件付きで出力するよう変更。 +- `templates/deployment.yaml`: `wp-config.php` 内のPHP側REMOTE_ADDR再判定処理を削除(NginxのfastcgiパラメータでREMOTE_ADDRは既に正しく渡っているため不要かつ危険だった)。 +- `values.yaml`/`README.md`: `nginx.forwardRealIP` は本当にXFFを付与するリバースプロキシが前段にある場合のみ有効化する旨を明記。 + +**(2) メール送信の不具合** + +原因: 使用しているAlpineベースのWordPressイメージには、PHPの `mail()`(`wp_mail()` が内部的に利用)を実際に配送するMTA(sendmail相当)が同梱されておらず、`sendmail_path` の送信先が存在しなかった。 + +**修正内容**([helmchart/phpfpm](https://git.cafepieters.com/helmchart/phpfpm) チャートと同一の手法を採用。ユーザー指定によりphpfpmの実装パターンをそのまま踏襲): +- `values.yaml` に `smtp.*` セクションを追加(`enabled`/`host`/`protocol`/`port`/`auth.*`/`from`/`tls.*`)。 +- `templates/configmap-smtp.yaml`(新規): `/etc/msmtprc` を生成するConfigMap。 +- `templates/secret-smtp.yaml`(新規): SMTPパスワードを格納するSecret。 +- `templates/deployment.yaml`: `smtp.enabled: true` の場合のみ、`wordpress` コンテナの起動コマンドをシェルラッパーに変更し、`apk add msmtp ca-certificates` → `/etc/msmtprc` 配置(`chmod 644`、www-dataから読めるように) → PHPの `sendmail_path` を `msmtp -t` に向ける `99-smtp.ini` を生成 → `exec php-fpm` という順で起動する。 + - `apk add` にrootが必要なため、SMTP有効時のみ既存の `securityContext: runAsUser/runAsGroup: 82` を外している(PHP-FPMワーカー自体はイメージ標準の `www.conf` によりuid82で動作するため、実際のPHPコード実行権限は変わらない)。`smtp.enabled: false`(デフォルト)では従来どおり非rootで起動する。 + - msmtpのパスワードはmsmtprcに直書きせず `passwordeval "cat /etc/smtp-secrets/password"` で別ファイル参照(phpfpmの `CHANGELOG-8.5.6-f/g.md` で判明した「Secret/ConfigMapの権限が0600だとwww-dataから読めない」問題を踏まえ、最初から `0644` で実装済み)。 +- WordPress本体は `wp_mail()` が自動的に `mail()`/`sendmail_path` を経由するため、phpfpmと異なりPHPコード側の変更(ヘルパークラスの読み込み等)は不要。 + +**対象ファイル**: `values.yaml`, `templates/service.yaml`, `templates/deployment.yaml`, `templates/configmap-smtp.yaml`(新規), `templates/secret-smtp.yaml`(新規), `README.md`, `Chart.yaml` diff --git a/Chart.yaml b/Chart.yaml index d324e64..883a73c 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: wordpress-nginx description: WordPress with Nginx and PHP-FPM on Kubernetes type: application -version: 7.0.2 +version: 7.0.2-a appVersion: "7.0.2" keywords: - wordpress diff --git a/README.md b/README.md index 3769b6a..51687a3 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,73 @@ helm install my-wordpress ./wordpress-nginx -f custom-values.yaml | `wordpress.adsTxt.enabled` | ads.txtを有効化 | `false` | | `wordpress.adsTxt.content` | ads.txtの内容 | `""` | +### SMTP設定(メール送信) + +WordPressコンテナ内にはメール送信を行うMTA(sendmail相当のプログラム)が存在しないため、デフォルトのままではパスワードリセットメールやコメント通知などの `wp_mail()` によるメール送信ができません。外部SMTPリレー(Gmail、SendGrid、自前のメールサーバー等)経由で送信する場合は以下を設定してください。 + +| パラメータ | 説明 | デフォルト値 | +|-----------|------|-------------| +| `smtp.enabled` | SMTP機能有効化 | `false` | +| `smtp.host` | SMTPサーバーホスト名 | `smtp.example.com` | +| `smtp.protocol` | プロトコル(auto/starttls/tls) | `auto` | +| `smtp.port` | ポート番号 | `587` | +| `smtp.auth.enabled` | 認証有効化 | `true` | +| `smtp.auth.username` | SMTPユーザー名 | `smtp-user@example.com` | +| `smtp.auth.password` | SMTPパスワード | `""`(Secretで指定) | +| `smtp.from` | 送信元メールアドレス(固定) | `noreply@example.com` | +| `smtp.tls.verify` | TLS証明書検証 | `true` | +| `smtp.tls.allowSelfSigned` | 自己署名証明書を許可 | `false` | + +有効化すると、WordPressコンテナ起動時に `msmtp` を導入し、PHPの `sendmail_path` を `msmtp` 経由に設定します。`wp_mail()` はコード変更なしにそのままSMTP経由で送信されるようになります。 + +**設定例**: + +```yaml +smtp: + enabled: true + host: "smtp.gmail.com" + protocol: "starttls" + port: 587 + auth: + enabled: true + username: "your-email@gmail.com" + password: "" # --set smtp.auth.password='your-app-password' で指定 + from: "noreply@your-domain.com" +``` + +```bash +helm upgrade --install my-wordpress . \ + -f values.yaml \ + --set smtp.auth.password='your-app-password' +``` + +**よくある設定**: + +| プロバイダ | ホスト | ポート | プロトコル | +|-----------|-------|--------|----------| +| Gmail | smtp.gmail.com | 587 | starttls | +| Office365 | smtp.office365.com | 587 | starttls | +| Sendgrid | smtp.sendgrid.net | 587 | starttls | +| Amazon SES | email-smtp.region.amazonaws.com | 587 | starttls | + +**注意**: `smtp.enabled: true` の場合、msmtpのインストール(`apk add`)のためWordPressコンテナはrootとして起動し、セットアップ完了後に `php-fpm` へexecします。PHP-FPMのワーカープロセス自体はイメージ標準の設定により引き続き `www-data`(非root)で動作するため、実際のPHPコード実行権限は変わりません。`smtp.enabled: false`(デフォルト)の場合は従来どおり非rootユーザー(uid82)でコンテナが起動します。 + ### Service設定 | パラメータ | 説明 | デフォルト値 | |-----------|------|-------------| | `service.type` | Serviceタイプ | `LoadBalancer` | | `service.port` | Serviceポート | `80` | +| `service.externalTrafficPolicy` | 外部トラフィックポリシー(`LoadBalancer`/`NodePort`のみ有効) | `Local` | + +**実クライアントIPの取得について(ベアメタル/MetalLB構成)**: + +このチャートのデフォルト構成(ルーター → MetalLB等のLoadBalancer Service → Pod)では、`service.externalTrafficPolicy` が `Cluster`(Kubernetesのデフォルト)だと、kube-proxyが送信元IPをノードの内部IPにSNATしてしまい、Nginx/WordPressが実際の訪問者IPを取得できません。本チャートではデフォルトで `externalTrafficPolicy: Local` を設定し、SNATを回避することで `$remote_addr` に実IPがそのまま渡るようにしています。 + +- **注意**: `Local` にすると、リクエストを受けたノードにPodが存在しない場合はそのノードでの接続が失敗します。MetalLB L2モードなど、Podが存在するノードにのみトラフィックが向く構成と組み合わせて使用してください(複数ノードにPodを分散させることを推奨)。 +- `service.type: ClusterIP` の場合、`externalTrafficPolicy` は無視されます(Kubernetes仕様上ClusterIPには適用不可のため)。 + +`nginx.forwardRealIP` は、Ingress ControllerやCDN(Cloudflareなど)、外部LBのように **X-Forwarded-For ヘッダーを付与するリバースプロキシを前段に置く場合にのみ**有効にしてください。本チャートのデフォルト構成(Service直下にPodがぶら下がる構成)ではX-Forwarded-Forを付与する層が存在しないため、`forwardRealIP` を有効にしても効果はなく、`service.externalTrafficPolicy: Local` のみで実IPが取得できます。 ### Ingress設定 diff --git a/templates/configmap-smtp.yaml b/templates/configmap-smtp.yaml new file mode 100644 index 0000000..adbdf86 --- /dev/null +++ b/templates/configmap-smtp.yaml @@ -0,0 +1,48 @@ +{{- if .Values.smtp.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "wordpress-nginx.fullname" . }}-smtp-config + labels: + {{- include "wordpress-nginx.labels" . | nindent 4 }} +data: + msmtprc: | + # msmtp設定ファイル + # PHPのsendmail_pathから呼び出される(wp_mail()経由の送信もこれを使用) + + defaults + {{- if .Values.smtp.tls.verify }} + tls on + tls_trust_file /etc/ssl/certs/ca-certificates.crt + {{- else }} + tls off + {{- end }} + {{- if .Values.smtp.tls.allowSelfSigned }} + tls_certcheck off + {{- else }} + tls_certcheck on + {{- end }} + + account default + host {{ .Values.smtp.host }} + port {{ .Values.smtp.port }} + {{- if eq .Values.smtp.protocol "starttls" }} + protocol smtp + {{- else if eq .Values.smtp.protocol "tls" }} + protocol smtps + {{- else if eq (int .Values.smtp.port) 465 }} + protocol smtps + {{- else }} + protocol smtp + {{- end }} + + {{- if .Values.smtp.auth.enabled }} + auth on + user {{ .Values.smtp.auth.username }} + passwordeval "cat /etc/smtp-secrets/password" + {{- else }} + auth off + {{- end }} + + from {{ .Values.smtp.from }} +{{- end }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml index e7efcc6..18b2650 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -69,15 +69,10 @@ spec: $_SERVER['HTTPS'] = 'on'; } - {{- if .Values.nginx.forwardRealIP.enabled }} - // Add Trusted Proxy - Extract Real Client IP from X-Forwarded-For header - if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $forwarded_ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); - $_SERVER['REMOTE_ADDR'] = trim($forwarded_ips[0]); - } elseif (isset($_SERVER['HTTP_X_REAL_IP'])) { - $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP']; - } - {{- end }} + // 実クライアントIPの解決はNginx側(real_ipモジュール)で完結しており、 + // fastcgi_param REMOTE_ADDR として渡されるためPHP側での再判定は行わない。 + // ここでX-Forwarded-Forを無条件に信頼して上書きすると、Nginxの信頼済み + // プロキシ判定を迂回してクライアントがIPを詐称できてしまうため実施しない。 $protocol = 'http'; if ( isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) { @@ -291,10 +286,38 @@ spec: - name: wordpress image: "{{ .Values.image.wordpress.registry }}/{{ .Values.image.wordpress.repository }}:{{ .Values.image.wordpress.tag }}" imagePullPolicy: {{ .Values.image.wordpress.pullPolicy }} + {{- if .Values.smtp.enabled }} + # SMTP有効時はapk addでmsmtpを導入する必要があるためrootで起動し、 + # セットアップ後にphp-fpmへexecする(ワーカープロセス自体はイメージ標準の + # www.confによりwww-data(uid82)で動作するため実処理は非rootのまま) + command: ["/bin/sh", "-c"] + args: + - | + set -e + echo "Configuring SMTP for mail sending..." + apk add --no-cache msmtp ca-certificates + + # msmtpはPHP-FPMワーカー(www-data等の非root)から実行されるため、 + # rootが作成するこのファイルも読み取り可にする必要がある + # (実パスワードはpasswordeval経由で別ファイルから取得するため、 + # msmtprc自体に平文パスワードは含まれない) + cp /etc/smtp-config/msmtprc /etc/msmtprc + chmod 644 /etc/msmtprc + + cat > /usr/local/etc/php/conf.d/99-smtp.ini << 'PHP_SMTP_EOF' + ; SMTP設定 - msmtp経由でメール送信 + sendmail_path = "/usr/bin/msmtp -t" + sendmail_from = "{{ .Values.smtp.from }}" + PHP_SMTP_EOF + + echo "SMTP configured: {{ .Values.smtp.host }}:{{ .Values.smtp.port }}" + exec php-fpm + {{- else }} command: ["php-fpm"] securityContext: runAsUser: 82 runAsGroup: 82 + {{- end }} env: - name: WORDPRESS_DB_HOST value: {{ .Values.wordpress.dbHost | quote }} @@ -314,6 +337,16 @@ spec: mountPath: /var/www/html - name: wordpress-persistent mountPath: /var/www/html/wp-content + {{- if .Values.smtp.enabled }} + - name: smtp-config + mountPath: /etc/smtp-config + readOnly: true + {{- if .Values.smtp.auth.enabled }} + - name: smtp-secrets + mountPath: /etc/smtp-secrets + readOnly: true + {{- end }} + {{- end }} resources: {{- toYaml .Values.resources.wordpress | nindent 12 }} volumes: @@ -334,6 +367,26 @@ spec: configMap: name: {{ include "wordpress-nginx.fullname" . }}-adstxt {{- end }} + {{- if .Values.smtp.enabled }} + - name: smtp-config + configMap: + name: {{ include "wordpress-nginx.fullname" . }}-smtp-config + items: + - key: msmtprc + path: msmtprc + {{- if .Values.smtp.auth.enabled }} + - name: smtp-secrets + secret: + secretName: {{ include "wordpress-nginx.fullname" . }}-smtp + items: + - key: password + path: password + # 0600だとSecretボリュームの所有者はroot、 + # msmtpはPHP-FPMワーカー(www-data等の非root)から実行されるため読めない + # → 全ユーザー読み取り可にする(ボリュームはPod内にしか存在しないため許容範囲) + mode: 0644 + {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/templates/secret-smtp.yaml b/templates/secret-smtp.yaml new file mode 100644 index 0000000..cf92b19 --- /dev/null +++ b/templates/secret-smtp.yaml @@ -0,0 +1,13 @@ +{{- if .Values.smtp.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "wordpress-nginx.fullname" . }}-smtp + labels: + {{- include "wordpress-nginx.labels" . | nindent 4 }} +type: Opaque +data: + {{- if .Values.smtp.auth.enabled }} + password: {{ .Values.smtp.auth.password | b64enc }} + {{- end }} +{{- end }} diff --git a/templates/service.yaml b/templates/service.yaml index d96fffa..ef21aef 100644 --- a/templates/service.yaml +++ b/templates/service.yaml @@ -6,6 +6,11 @@ metadata: {{- include "wordpress-nginx.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} + {{- if and .Values.service.externalTrafficPolicy (ne .Values.service.type "ClusterIP") }} + # ベアメタル環境(MetalLB等)でkube-proxyのSNATを回避し、クライアントの実IPを + # $remote_addr にそのまま渡すための設定。LoadBalancer/NodePortでのみ有効。 + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} ports: - port: {{ .Values.service.port }} targetPort: http diff --git a/values.yaml b/values.yaml index e7739af..c68fa9a 100644 --- a/values.yaml +++ b/values.yaml @@ -44,9 +44,47 @@ wordpress: # ads.txt content # google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0 +# SMTP設定(wp_mail()/PHPのmail()関数によるメール送信用) +# コンテナ内にはメール送信を行うMTA(sendmail相当)が存在しないため、 +# デフォルトのままではWordPressからのメール(パスワードリセット、通知等)は送信できません。 +# 外部SMTPリレー(Gmail、SendGrid、自前のメールサーバー等)経由で送信する場合に有効化してください。 +smtp: + enabled: false + + # SMTPサーバー設定 + host: "smtp.example.com" + + # プロトコルとポート設定 + # - auto: 自動判定(推奨) + # - starttls: STARTTLS(ポート587) + # - tls: SSL/TLS(ポート465) + protocol: "auto" + port: 587 + + # 認証設定 + auth: + enabled: true + username: "smtp-user@example.com" + # password はSecretで管理する + # helm install ... --set smtp.auth.password='your-password' 等で指定 + password: "" + + # 送信元アドレス(固定値。wp-config.phpやプラグイン側の指定より優先されます) + from: "noreply@example.com" + + # TLS/SSL設定 + tls: + # 証明書検証を有効化(本番環境では true を推奨) + verify: true + # 自己署名証明書を許可する場合(テスト環境のみ) + allowSelfSigned: false + nginx: - # ベアメタルクラスター等でリアルIPを取得する設定 - # ローカルIP(ベアメタル等)から訪問者のリアルIPを取得する場合に有効にします + # Ingress ControllerやCDN(Cloudflare等)、外部LB等、X-Forwarded-Forヘッダーを + # 付与するリバースプロキシを前段に置く場合にのみ有効にしてください。 + # 本チャートのデフォルト構成(Service直下にPodがぶら下がるベアメタル/MetalLB構成)では + # X-Forwarded-For を付与する層が存在しないため、この設定を有効にしても効果はありません。 + # その場合は下記の service.externalTrafficPolicy: Local を使用してください。 forwardRealIP: enabled: false # 信頼できるプロキシのIPレンジを追加してください @@ -66,6 +104,13 @@ service: # type: ClusterIP port: 80 targetPort: 80 + # ベアメタルクラスター(MetalLB等)でクライアントの実IPを取得するための設定。 + # kube-proxyはデフォルト(Cluster)だと送信元IPをノードIPにSNATしてしまうため、 + # Local にすることでSNATを回避し、Nginxの$remote_addrに実IPがそのまま渡ります。 + # 注意: Local にすると、Podが存在しないノードへのアクセスは失敗するため、 + # DaemonSetまたは全ノードにPodが分散する構成を推奨します。 + # LoadBalancer / NodePort でのみ有効(ClusterIPでは無視されます)。 + externalTrafficPolicy: Local # Ingress設定 ingress: