Net-Base Magazin

23.05.2026

Reverse Proxy mit nginx und Delphi: sauberes Forwarded-Handling, echte Client-IP und robuste URL-Basen

Wenn Delphi-REST-Server hinter nginx laufen, kippen oft Client-IP, HTTPS-Erkennung und absolute URLs. Dieser Source-Schnipsel zeigt ein robustes Forwarded-/X-Forwarded-Handling (inkl. Trust-Proxy-Liste), typische nginx-Settings und Debugging-Hinweise für den Betrieb.

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

Zweck: Die Applikation bekommt Host, Client-IP und Schema zuverlässig weitergereicht. Randbedingung: $proxy_add_x_forwarded_for hängt die aktuelle Proxy-IP an eine evtl. vorhandene Kette an; das ist gut für Multi-Proxy-Setups, macht aber die korrekte Auswertung auf Delphi-Seite umso wichtiger. Stolperfall: Wenn Sie in nginx den Host-Header nicht setzen, sieht Delphi ggf. nur den Upstream-Host (127.0.0.1), was Redirects und Origin-Checks bricht.

Delphi Source-Schnipsel: Forwarded/X-Forwarded robust auswerten (mit Trust-Proxy-Liste)

Der folgende Code ist bewusst framework-neutral gehalten: Er arbeitet gegen ein minimales Interface (Header + RemoteIP) und lässt sich in WebBroker, RAD Server oder Horse adaptieren. Kernpunkte:

  • Priorität: RFC Forwarded (falls vorhanden) vor X-Forwarded-*.
  • Trust: Forwarded-Header nur auswerten, wenn der direkte Peer (RemoteIP) ein bekannter Proxy ist.
  • Parsing: IPv6, Quotes, Ports und Ketten in X-Forwarded-For berücksichtigen.
  • Output: eine Base-URL, die Sie für absolute Links, Redirects oder OpenAPI verwenden können.
Delphi
unit NetBase.ProxyForwarding;

interface

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

type
  // Minimales Adapter-Interface: implementieren Sie es für WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // direkte TCP-Gegenstelle (meist nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // z.B. 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 kann "client, proxy1, proxy2" sein
  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 in []: [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: host:port (Achtung: bei nacktem IPv6 ohne [] nicht zuverlässig)
  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
  // Beispiel: 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;

  // Mehrere Elemente sind per Komma getrennt; wir nehmen das erste (client-naheste)
  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 kann IP oder "unknown" sein; IPv6 kann in [] stehen
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 kommt vor
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // defensiv
      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
  // Für echte IPv6-Normalisierung bräuchte man ein IP-Parsing; hier bewusst pragmatisch.
  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; // Fallback: direkte Gegenstelle

  // Nur wenn die direkte Gegenstelle ein bekannter Proxy ist, werten wir Forwarded-Header aus.
  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-* als Fallback/Ergänzung
    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;

  // Notfalls Host aus "Host" Header nehmen (wenn kein Proxy-Header da ist)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Zweck: Sie bekommen aus jedem Request eine konsistente Sicht auf ClientIP, Proto und Host sowie eine BaseUrl. Diese Informationen können Sie zentral für Logging, Sicherheitsentscheidungen (z. B. IP-Allowlist) und Link-Generierung nutzen.

Warum die Trust-Proxy-Liste nötig ist: Ohne Trust-Prüfung könnte ein Angreifer direkt Ihren Delphi-Port erreichen (Fehlkonfiguration, internes Routing, VPN) und einfach X-Forwarded-For: 127.0.0.1 schicken. Damit wären Audit-Trails, Rate-Limits oder „nur intern erlaubt“-Endpunkte angreifbar. Vertrauen Sie Forwarded-Headern nur, wenn der direkte Peer (RemoteIP) ein Proxy ist, den Sie kontrollieren (z. B. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Stolperfallen: IPv6 ohne eckige Klammern ist in Host:port-Notation nicht eindeutig. Im HTTP-Host-Header ist IPv6 normalerweise in [] notiert; halten Sie sich daran. Für komplexe IP-Ranges (CIDR) müssten Sie die Trust-Liste erweitern (z. B. durch ein echtes IP-Parsing).

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

In WebBroker (TWebRequest) kommen Header typischerweise über ContentFields oder GetFieldByName, Remote-IP je nach Server-Backend. In Horse (oder anderen HTTP-Frameworks) gibt es meist Req.Headers und eine Remote-IP-Eigenschaft. Wichtig ist das Prinzip: RemoteIP muss die TCP-Gegenstelle sein, nicht irgendein Headerwert.

Praktisch bewährt: Erzeugen Sie beim Service-Start eine TTrustedProxyList aus Konfiguration (INI/ENV), z. B. „127.0.0.1“ für lokale nginx-Setups oder die IP Ihres Load Balancers. Dann rufen Sie ResolveForwardedInfo pro Request auf und schreiben die Felder in Ihr strukturiertes Logging (JSON-Log, Syslog oder Windows Event Log).

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

Wenn Requests „komisch“ wirken, liegt es selten an Delphi-HTTP selbst, sondern an einer Kombination aus Proxy-Headern, Redirect-Logik und Timeouts. Drei Debug-Checks, die sich im Alltag lohnen:

  1. Header-Dump (gezielt): Loggen Sie bei 4xx/5xx zusätzlich Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent und Request-URI. Aber nur bei Fehlern – sonst wird das Log teuer und unübersichtlich.
  2. Base-URL prüfen: Wenn Redirects oder Callback-URLs fehlschlagen, loggen Sie ForwardedInfo.BaseUrl. Viele Fehler sind sofort sichtbar („http://127.0.0.1“ statt „https://api…“).
  3. Timeout-Korrelation: Ein 504 vom Proxy ist nicht dasselbe wie ein Delphi-Timeout. nginx proxy_read_timeout und Delphi-seitige Idle-/Read-Timeouts müssen zusammenpassen.

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

WebSockets hinter nginx

Für WebSockets braucht nginx Upgrade und Connection korrekt. Zusätzlich darf das Backend nicht „zu früh“ schließen. Auf Delphi-Seite ist relevant, dass Ihre WebSocket-Komponente (oder ein SSE/Streaming-Endpunkt) mit Reverse Proxies umgehen kann und Heartbeats/Keep-Alives sauber implementiert sind.

Große Uploads und 413-Fehler

Ein Klassiker: Delphi akzeptiert einen Upload, aber nginx blockt vorher mit 413 Request Entity Too Large. Steuern Sie das explizit über client_max_body_size und passen Sie Delphi-seitig Request-Limits an. Für prozessnahe Softwarelösungen mit Dokumenten- oder Bilddaten ist das kein Sonderfall, sondern Normalbetrieb.

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.

Beitrag teilen

Diesen Beitrag direkt weitergeben

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

E-Mail

Instagram oeffnet in einem neuen Tab. Link und Kurztext werden vorher in die Zwischenablage kopiert.