Net-Base マガジン

23.05.2026

nginx と Delphi を用いたリバースプロキシ:Forwarded ヘッダの正確な処理、実際のクライアントIP、堅牢なURLベース

Delphi-REST-サーバが nginx の背後で動作していると、クライアントIP、HTTPS検出、絶対URL がしばしば正しく処理されなくなります。 このソーススニペットは、堅牢な Forwarded/X-Forwarded ヘッダ処理(Trust-Proxy リストを含む)、典型的な nginx 設定、および運用時のデバッグ向けヒントを示します。

23.05.2026

Ein Reverse Proxy mit nginx und Delphi ist in der Praxis meist kein „nice to have“, sondern die saubere Trennung zwischen Internetkante und Applikation: TLS-Terminierung (HTTPS-Offloading), zentrale Header-/CORS-Regeln, Rate-Limits, einheitliche Logs, Blue/Green-Rollouts oder einfach das Hosting mehrerer Services unter einer Domain. Was dann gerne unterschätzt wird: Sobald nginx „davor“ sitzt, sieht der Delphi-Server nur noch die Proxy-IP, oft nur noch „http“ statt „https“ und generiert falsche absolute Links (Redirects, Callback-URLs, OpenAPI-Server-URL). Genau diese drei Punkte sorgen später für Debugging-Zeit im Betrieb.

Dieser Source-Schnipsel zeigt ein robustes Muster, wie Sie in Delphi Forwarded bzw. X-Forwarded-* sauber auswerten – inklusive Trust-Proxy-Liste (wichtig gegen Header-Spoofing) und einer konsistenten Request-Base-URL. Dazu gibt es praxistaugliche nginx-Konfigurationen und Hinweise zu Randfällen wie WebSockets, große Uploads und Timeouts.

Warum Reverse Proxy-Setups Delphi-Server „verwirren“

nginx spricht als Reverse Proxy mit dem Delphi-Service typischerweise unverschlüsselt (HTTP) im internen Netz oder auf localhost, während der Client außen per HTTPS kommt. Ohne zusätzliche Header weiß Delphi nichts von:

  • Original-Schema (https vs. http) – relevant für Redirects und absolute URLs.
  • Original-Host (kundenspezifische Domain, Port) – relevant für Multi-Tenant-Setups, CORS und Callback-URLs.
  • Original-Client-IP – relevant für Audit, Rate-Limits, Geo-Checks und Security-Auswertungen.

nginx kann diese Informationen über Header transportieren. Üblich sind X-Forwarded-For, X-Forwarded-Proto und X-Forwarded-Host; standardisiert ist zusätzlich der RFC-Header Forwarded. Wichtig: Diese Header sind aus Sicht der Applikation nicht automatisch vertrauenswürdig, weil ein Client sie selbst schicken kann – sie werden erst vertrauenswürdig, wenn sie von einem bekannten Proxy stammen.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Ein solider Startpunkt (HTTP/1.1, Keep-Alive, Upgrade für WebSockets) sieht so aus. Das Snippet ist bewusst knapp gehalten; Sie ergänzen je nach Umgebung HSTS, Rate-Limits und Access-Logs.

Delphi
# (nginx-Konfiguration, kein Delphi)
server {
  listen 443 ssl http2;
  server_name api.example.com;

  # ssl_certificate ...; ssl_certificate_key ...;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # optional, aber praktisch für absolute URLs
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;

    # WebSockets / Upgrade
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header Connection        $connection_upgrade;

    proxy_pass http://127.0.0.1:8080;

    # Timeouts passend zu Delphi-Backend (lange Reports/Exports)
    proxy_connect_timeout 5s;
    proxy_send_timeout    60s;
    proxy_read_timeout    60s;

    # große Uploads explizit steuern
    client_max_body_size 50m;
  }
}

目的: アプリケーションは Host、Client-IP およびスキーマを確実に受け渡されます。 前提条件: $proxy_add_x_forwarded_for は現在のプロキシIPを既存のチェーンに追加します。これはマルチプロキシ構成では有用ですが、Delphi 側での正確な評価を一層重要にします。 落とし穴: nginx で Host ヘッダーを設定しないと、Delphi は場合によってはアップストリームホスト(127.0.0.1)しか認識せず、リダイレクトやオリジンチェックが機能しなくなります。

Delphi ソーススニペット: Forwarded/X-Forwarded を堅牢に評価する(Trust-Proxyリストを用いた)

以下のコードはあえてフレームワークに依存しない形で記述しています: 最小限のインターフェース(Header + RemoteIP)を想定して動作し、WebBroker、RAD Server または Horse に適合させられます。要点:

  • Priorität: RFC Forwarded(存在する場合)は X-Forwarded-* より優先されます。
  • Trust: Forwarded-Header は直接のピア(RemoteIP)が既知のプロキシである場合にのみ評価します。
  • Parsing: IPv6、引用符、ポート、および X-Forwarded-For 内のチェーンを考慮します。
  • Output: 絶対リンク、リダイレクト、または OpenAPI に使用できる Base-URL を生成します。

unit Net-Base.ProxyForwarding;

interface

uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;

type
// 最小限のアダプタインターフェース: WebBroker/Horse 等に対して実装してください。
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // 直接のTCPピア(通常は nginx)
end;

TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // 例: https://api.example.com
end;

TTrustedProxyList = class
private
FSet: TDictionary;
class function NormalizeIp(const AIP: string): string; static;
public
constructor Create;
destructor Destroy; override;
procedure Add(const AIP: string);
function Contains(const AIP: string): Boolean;
end;

function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;

implementation

function StripBrackets(const S: string): string;
begin
Result := S.Trim;
if (Result.StartsWith(‚[‚)) and (Result.EndsWith(‚]‘)) then
Result := Result.Substring(1, Result.Length – 2);
end;

function RemoveQuotes(const S: string): string;
var
T: string;
begin
T := S.Trim;
if (T.Length >= 2) and (((T[1] = ‚“‚) and (T[T.Length] = ‚“‚)) or ((T[1] = ““) and (T[T.Length] = ““))) then
Result := T.Substring(1, T.Length – 2)
else
Result := T;
end;

function FirstCsvToken(const S: string): string;
var
P: Integer;
begin
// X-Forwarded-For は „client, proxy1, proxy2″ のようになり得る
P := S.IndexOf(‚,‘);
if P >= 0 then
Result := S.Substring(0, P).Trim
else
Result := S.Trim;
end;

procedure SplitHostPort(const HostPort: string; out Host: string; out Port: Integer);
var
S: string;
P: Integer;
begin
Host := “;
Port := 0;
S := HostPort.Trim;

// IPv6 は [] で囲まれる: [2001:db8::1]:443
if S.StartsWith(‚[‚) then
begin
P := S.IndexOf(‚]‘);
if P >= 0 then
begin
Host := StripBrackets(S.Substring(0, P + 1));
if (P + 1 < S.Length) and (S[P + 2] = ‚:‘) then
Port := StrToIntDef(S.Substring(P + 2), 0);
Exit;
end;
end;

// IPv4/ホスト: host:port (注意: 生のIPv6([]なし)では信頼できない)
P := S.LastIndexOf(‚:‘);
if (P > 0) and (S.IndexOf(‚:‘) = P) then
begin
Host := S.Substring(0, P);
Port := StrToIntDef(S.Substring(P + 1), 0);
end
else
Host := S;
end;

function ParseForwardedHeader(const ForwardedValue: string; out ClientIP, Proto, Host: string): Boolean;
var
// 例: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
Parts: TArray<string>;
I: Integer;
KV: TArray<string>;
K, V: string;
FirstElement: string;
begin
Result := False;
ClientIP := “;
Proto := “;
Host := “;

if ForwardedValue.Trim = “ then
Exit;

// 複数要素はカンマ区切り; 最初の要素(クライアントに最も近い)を取る
FirstElement := FirstCsvToken(ForwardedValue);
Parts := FirstElement.Split([‚;‘]);
for I := 0 to High(Parts) do
begin
KV := Parts[I].Split([‚=‘], 2);
if Length(KV) <> 2 then
Continue;
K := KV[0].Trim.ToLower;
V := RemoveQuotes(KV[1]);

if K = ‚for‘ then
begin
// for は IP または „unknown“ になり得る; IPv6 は [] で囲まれることがある
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// for=1.2.3.4:5678 のようなケースがある
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // 防御的に
ClientIP := StripBrackets(V.Split([‚:‘])[0]);
end
else if K = ‚proto‘ then
Proto := V.Trim.ToLower
else if K = ‚host‘ then
Host := V.Trim;
end;

Result := (ClientIP <> “) or (Proto <> “) or (Host <> “);
end;

{ TForwardedInfo }

function TForwardedInfo.EffectiveScheme: string;
begin
if Proto <> “ then
Exit(Proto);
Result := ‚http‘;
end;

function TForwardedInfo.EffectiveHostPort: string;
begin
if Host = “ then
Exit(“);

if (Port > 0) and not ((EffectiveScheme = ‚https‘) and (Port = 443)) and not ((EffectiveScheme = ‚http‘) and (Port = 80)) then
Result := Host + ‚:‘ + Port.ToString
else
Result := Host;
end;

function TForwardedInfo.BaseUrl: string;
var
HP: string;
begin
HP := EffectiveHostPort;
if HP = “ then
Exit(“);
Result := EffectiveScheme + ‚://‘ + HP;
end;

{ TTrustedProxyList }

constructor TTrustedProxyList.Create;
begin
inherited Create;
FSet := TDictionary<string, Boolean>.Create;
end;

destructor TTrustedProxyList.Destroy;
begin
FSet.Free;
inherited;
end;

class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
// 実際にIPv6を正規化するにはIPパースが必要だが、ここでは意図的に実用的な簡易処理をしている。
Result := AIP.Trim;
Result := StripBrackets(Result);
end;

procedure TTrustedProxyList.Add(const AIP: string);
begin
FSet.AddOrSetValue(NormalizeIp(AIP), True);
end;

function TTrustedProxyList.Contains(const AIP: string): Boolean;
begin
Result := FSet.ContainsKey(NormalizeIp(AIP));
end;

function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;
var
RemoteIP: string;
Fwd, XFF, XProto, XHost, XPort: string;
ClientIP, Proto, Host: string;
Port: Integer;
begin
Result := Default(TForwardedInfo);

RemoteIP := Req.GetRemoteIP;
Result.ClientIP := RemoteIP; // フォールバック: 直接のピア

// 直接ピアが既知のプロキシである場合に限り、Forwarded ヘッダを評価する。
if (TrustedProxies <> nil) and TrustedProxies.Contains(RemoteIP) then
begin
Fwd := Req.GetHeaderValue(‚Forwarded‘);
if ParseForwardedHeader(Fwd, ClientIP, Proto, Host) then
begin
if ClientIP <> “ then Result.ClientIP := ClientIP;
if Proto <> “ then Result.Proto := Proto;
if Host <> “ then
begin
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

// X-Forwarded-* はフォールバック/補完として扱う
XFF := Req.GetHeaderValue(‚X-Forwarded-For‘);
if (Result.ClientIP = RemoteIP) and (XFF.Trim <> “) then
Result.ClientIP := StripBrackets(FirstCsvToken(XFF));

XProto := Req.GetHeaderValue(‚X-Forwarded-Proto‘);
if (Result.Proto = “) and (XProto.Trim <> “) then
Result.Proto := FirstCsvToken(XProto).ToLower;

XHost := Req.GetHeaderValue(‚X-Forwarded-Host‘);
if (Result.Host = “) and (XHost.Trim <> “) then
Result.Host := FirstCsvToken(XHost);

XPort := Req.GetHeaderValue(‚X-Forwarded-Port‘);
Port := StrToIntDef(FirstCsvToken(XPort), 0);
if (Result.Port = 0) and (Port > 0) then
Result.Port := Port;
end;

// 最終手段として „Host“ ヘッダから Host を取得する(プロキシヘッダがない場合)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

end.

目的: 各リクエストからClientIPProtoHost、およびBaseUrlに関する一貫したビューを得られます。これらの情報は中央でのログ記録、セキュリティ判断(例: IP許可リスト)やリンク生成に利用できます。

なぜ Trust-Proxy-Liste が必要か: Trustチェックを行わなければ、攻撃者が直接Delphiのポートに到達(設定ミス、内部ルーティング、VPN 等)し、単にX-Forwarded-For: 127.0.0.1を送信することで、不正に振る舞う可能性があります。これでは監査トレイル、レート制限、あるいは「内部のみ許可」エンドポイントが攻撃に晒されます。Forwarded ヘッダは、直接のピア(RemoteIP)が貴社管理下のプロキシ(例: 127.0.0.1、ロードバランサのIP、Kubernetes Ingress)である場合にのみ信頼してください。

注意点: IPv6 を角括弧なしで Host:port 表記にすると曖昧になります。HTTP の Host ヘッダ内では IPv6 は通常 [] で表記されるため、これに従ってください。複雑な IP 範囲(CIDR)を扱う場合は、Trust リストを拡張して本格的な IP パースを実装する必要があります。

Integration in WebBroker/Horse/RAD Server: wo der Code „andockt“

WebBroker(TWebRequest)ではヘッダは通常 ContentFieldsGetFieldByName 経由で、Remote-IP はサーババックエンドに依存して取得されます。Horse(または他の HTTP フレームワーク)では一般に Req.Headers と Remote-IP プロパティが存在します。重要なのは原則であり、RemoteIP は TCP の相手先でなければならず、任意のヘッダ値であってはなりません。

実務上の手順としては、サービス起動時に設定(INI/ENV)から TTrustedProxyList を生成します。例としてローカル nginx セットアップでは「127.0.0.1」、あるいはロードバランサの IP を登録します。その後、各リクエストで ResolveForwardedInfo を呼び出し、得られたフィールドを構造化ログ(JSON ログ、Syslog、または Windows Event Log)に書き込みます。

Debugging im Betrieb: so finden Sie Fehler in Minuten statt Stunden

リクエストの挙動が「おかしい」場合、多くは Delphi-HTTP 自体ではなく、プロキシヘッダ、リダイレクトロジック、タイムアウトの組み合わせに原因があります。運用で有効なデバッグチェックを三つ挙げます。

  1. Header-Dump(限定的に): 4xx/5xx 発生時に限り、HostForwardedX-Forwarded-ForX-Forwarded-ProtoUser-Agent、および Request-URI をログに出力してください。ただし常時出力するとログが肥大化し管理が難しくなります。
  2. Base-URL の確認: リダイレクトやコールバック URL が失敗する場合は ForwardedInfo.BaseUrl をログに残してください。多くの問題はここで即座に判明します(例: “http://127.0.0.1” のままになっていて “https://api…” ではない等)。
  3. タイムアウトの相関: プロキシからの 504 は Delphi 側のタイムアウトとは異なります。nginx の proxy_read_timeout と Delphi 側のアイドル/リードタイムアウトを整合させてください。

Randfälle: WebSockets, Streaming und große Requests

WebSockets hinter nginx

WebSocket を通す場合、nginx では UpgradeConnection が正しく設定されている必要があります。加えてバックエンドが「早すぎて」接続を切らないようにすることが重要です。Delphi 側では、WebSocket コンポーネント(あるいは SSE/ストリーミングのエンドポイント)がリバースプロキシに対応でき、ハートビートやキープアライブが適切に実装されていることが重要です。

Große Uploads und 413-Fehler

典型例として、Delphi はアップロードを受け入れても、前段の nginx が 413 Request Entity Too Large でブロックすることがあります。これを制御するには client_max_body_size を明示的に設定し、Delphi 側のリクエスト制限も調整してください。ドキュメントや画像データを扱うプロセス近傍のソリューションではこれは例外ではなく通常の運用です。

HTTPS-Offloading und „Secure Cookies“

Wenn Ihr Delphi-Service Session-Cookies setzt, müssen diese bei externem HTTPS in der Regel als Secure markiert sein. Ob Ihre Applikation das tut, hängt oft daran, ob sie „weiß“, dass der ursprüngliche Request HTTPS war. Genau hier hilft die konsistente Auswertung von X-Forwarded-Proto/Forwarded.

Wann sich der Aufwand lohnt – und wo er kippen kann

Der gezeigte Ansatz lohnt sich immer dann, wenn der Delphi-Service nicht mehr „nackt“ im LAN lebt, sondern Teil einer produktiven Kante ist: mehrere Domains, SSO/SAML-Oberflächen, Public APIs, Mandantenfähigkeit oder strengere Audit-Anforderungen. Er kippt dort, wo man Forwarded-Header blind vertraut oder Proxy-Topologien nicht dokumentiert (mehrere Ingress-Stufen, Cloud-LB plus nginx plus Sidecar). Dann werden Client-IP und Schema schnell „irgendwas“.

Eine klare Grenze: Wenn Sie komplexe Trust-Regeln brauchen (CIDR, IPv6-Netze, dynamische LB-IPs), sollten Sie die Trust-Prüfung ausbauen (echtes IP-Parsing, Netzmasken) oder die Infrastruktur so gestalten, dass nur ein definierter Proxy den Delphi-Port erreichen kann (Firewall/Security Groups). Das ist am Ende meist die robustere Betriebsentscheidung.

Fazit: Reverse Proxy mit nginx und Delphi sauber betreiben heißt „Forwarded richtig machen“

Ein Reverse Proxy mit nginx ist für Delphi-REST-Server ein guter Standardbaustein – aber erst die korrekte Behandlung von Forwarded und X-Forwarded-* macht das Setup im Betrieb stabil. Der Kern ist simpel: Header nur von vertrauenswürdigen Proxies akzeptieren, Client-IP/Scheme/Host konsistent ableiten und diese Basis in Redirects, Logging und Security-Checks durchziehen. Mit dem Snippet oben haben Sie dafür ein sauberes, legacy-taugliches Fundament, das sich in WebBroker, Horse oder eigene HTTP-Server integrieren lässt.

Wenn Sie ein bestehendes Delphi-Backend hinter nginx konsolidieren oder in Richtung Delphi REST-API und REST-Server mit klarer Betriebslinie modernisieren möchten, ist ein technisches Review der Proxy-Kette und der Header-Auswertung oft der schnellste Hebel. Kontaktieren Sie Net-Base für eine kurze technische Einordnung.

Im fachlichen Umfeld spielen auch Nginx Reverse Proxy und Forwarded Header eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

投稿を共有

この投稿を直接共有する

LinkedIn、X、XING、Facebook、WhatsApp、およびE-Mailはすぐに利用可能です。Instagram用のリンクと短文はただちに準備します。

Eメール

Instagramは新しいタブで開きます。リンクと短文は事前にクリップボードにコピーされます。