商店現已上線

我們打造用於網站、AI 智慧體和 LLM 訓練的 Workstation AI 機器——線上配置併購買。

獲取同行價格
AI 產品
AI 實驗室OpenAI 智慧體Claude 代理Grok BotWorkstation AI 原生 CRM營銷所有產品
AI 解決方案
應用型 AIAI 智慧體生成式 AIAI 自動化AI 技能提升中小企業 AI 套餐企業 AI 實驗室AI 工作站NVIDIA 硬體與軟體NVIDIA 加速計算私有 AIGPU 私有云GPU 叢集邊緣 AI行業 AI
AI 服務
平臺現代化數字工程資料基礎與 AI自主運營AI 諮詢DevOps 自動化網路安全軟體開發智慧體構建MLOps 搭建GPU 租賃GPU 私有云
關於我們
公司我們的故事合作伙伴客戶案例
AI 資料中心
AI 資料中心GPU 私有云GPU 叢集GPU 租賃
AI 洞察Jev 系列基準測試實驗室模型選擇
技術說明
文件
Linux 作業系統UbuntuRed Hat Enterprise LinuxSUSE LinuxAlpine LinuxDebian、Rocky 與 AlmaLinux
AI 雲平臺AWS 與 Amazon BedrockGoogle Cloud 與 Vertex AIMicrosoft Azure 與 AI FoundryGPU 雲與算力市場
私有 AI 資料中心面向律師事務所面向會計師事務所本地部署平臺全面安全的雲
工具Ring PromoterJobshout
新聞
部落格
聯絡我們登入

面向現代企業的 AI 工作站、AI 多智慧體軟體、GPU 基礎設施和智慧代理解決方案。

聯絡我們

AI 解決方案

應用型 AIAI 智慧體生成式 AIAI 自動化AI 技能提升中小企業 AI 套餐企業 AI 實驗室行業 AI

產品

所有產品Workstation AI native CRM 與 ERP營銷OpenAI 智慧體Ring PromoterJobshout

公司

關於我們公司AI 資料中心為什麼選擇Workstation合作伙伴客戶案例定價聯絡TenthMatrix

資源

AI 洞察技術說明文件Linux 作業系統AI 雲平臺私有 AI 資料中心部落格搜尋網站地圖
英國總部
77-79 Marlowes, Hemel Hempstead HP1 1LF路線指引 — 從 M25 外環倫敦 20 號出口駛出公司編號: 11641870週一至週五:上午 9:00 - 下午 6:00 GMT
+44 7515 356 146
比利時辦公室
Workstation SRL, Rue Vanderkindere 34, 1180 Uccle, BrusselsBE 0751.518.683週一至週五:上午 9:00 - 下午 6:00 CET
+32 492 45 67 46
印度辦公室
#159 Sector 9, Pocket 1, DDA Flats, 110077 Dwarka, New Delhi
+91 98881 98841
台灣辦公室
3F, No. 2-4, Aisi Rd., Ren'ai Dist., Keelung City 200, Taiwan基隆市仁愛區愛四路2之4號3樓週一至週五:上午 9:00 - 下午 6:00 臺灣時間(GMT+8)
+886 926 892 701+32 492 45 67 46

© 2026 Workstation AI。保留所有權利。

隱私資料刪除Cookie服務條款網站地圖

NVIDIA、NVIDIA 標誌、RTX、RTX PRO、Blackwell、CUDA、NVLink、MGX、Omniverse、Spectrum-X、Quantum、BlueField、DGX 與 HGX 為 NVIDIA Corporation 在美國及其他國家的商標及/或註冊商標。AMD、AMD Arrow 標誌、EPYC、Instinct、Ryzen 與 Threadripper 為 Advanced Micro Devices, Inc. 的商標。Dell 與 PowerEdge 為 Dell Inc. 或其子公司的商標。Intel 與 Xeon 為 Intel Corporation 或其子公司的商標。AMAX、AceleMax 與 LiquidMax 為 AMAX Engineering Corporation 的商標。所有其他產品名稱、標誌與品牌均為其各自所有者之財產。我們使用它們僅為識別我們所描述、供應或支援的產品;使用這些名稱並不表示其所有者為我們背書。

首頁 / 技術說明 / Technology
SecurityDevOpsOpen Source

How We Protected the Payments Secure FinTech App with WSLProxy Edge, an Open-Source API Gateway

The engineering write-up: threat model, the api_gw pipeline, every setting we used, the CORS incident we fixed in three minutes, and a checklist for your own API

2026年9月28日Technology15 min read

This is the engineering write-up. For a plain-English version, read the companion blog post.

How we protected the Payments Secure fintech app with WSLProxy Edge

In brief. We put the Payments Secure API behind the api_gw pipeline of WSLProxy Edge, our open-source gateway. It gives us Kong-class edge controls without running Kong: trusted real-IP resolution, correlation IDs, invalid-traffic (IVT) detection, Content-Type and body-size enforcement, route-aware sliding-window rate limits, and a redacted audit line per request. The whole policy is one JSON block in git, deployed by the same register workflow as our routing rules. We rolled it out to the integration API in audit mode first. One login regression surfaced within minutes and was fixed three minutes after that. This article covers the design, the exact settings, the incident and what we would do the same way again.

1. The app and the threat model

Payments Secure (payments-secure.fictionally.org) is a fictionalised fintech case study: customers authenticate, upload supporting documents, and call payment and account APIs. Behind the web front end sit two services:

  • OpsAPI, a Lapis/OpenResty service for accounts, authentication (password and Google OAuth), businesses and payment accounts, mounted under /opsapi.
  • FastAPI, a Python service for document ingestion, extraction and payment-related processing, mounted under /fastapi. Document uploads land on /fastapi/api/v1/statements.

Both are published through one hostname per environment. This article uses the integration API, int-api.payments-secure.fictionally.org, which our team and testers use daily and which runs the same edge software as production.

The threats we cared about are the ordinary ones any public API faces. They matter more here because of the data involved:

  • Opportunistic scanning. Bots probing for /.env, /.git/, /wp-admin and *.php. None of these exist on our stack, so any hit is by definition hostile or broken.
  • Credential stuffing and password spraying against the login endpoints.
  • Header spoofing. Requests that carry X-Internal-*, X-Consumer-* or X-Authenticated-* headers, hoping a backend trusts them as if an upstream component had set them.
  • IP spoofing via X-Forwarded-For. If the edge trusts the header from anyone, every per-IP control becomes optional for the attacker.
  • Resource exhaustion. Oversized bodies, unusual methods (TRACE, TRACK, CONNECT) and bursts that tie up workers doing expensive extraction.
  • Poor forensics. Without a shared request ID, a customer complaint cannot be joined to edge, OpsAPI and FastAPI logs.

2. Why the edge and not the application

We could have added middleware to each service. We chose not to, for three reasons:

  1. Two languages, one policy. OpsAPI is Lua and FastAPI is Python. The same rules written twice will drift apart.
  2. Reject early. A request rejected at the edge costs one table lookup and a small response. The same request rejected in FastAPI has already used a worker, parsed a body and maybe opened a database connection.
  3. The edge already exists. WSLProxy was already terminating traffic for the domain, routing to the cluster and running our WAF. A dedicated gateway such as Kong would have added another hop, another deployment, another database and another thing to patch.

WSLProxy's api_gw package was built for exactly this. A tenant adds an api_gw block to its server record and the next request goes through the pipeline. There is no nginx reload, no per-app gateway deployment and no Lua to write. A server with no api_gw block behaves exactly as before.

3. Where the gateway sits

Traffic arrives through a CDN, then reaches the WSLProxy edge node in London, which proxies to the Kubernetes cluster that runs the app. Inside WSLProxy the request path is:

gateway_ack.lua        (rewrite phase)
  cache → captcha-verify → load server → match rules → gateway_pipeline.execute:
      1. api_gw     ← the pipeline described here
      2. rate_limit (legacy server-level limiter)
      3. WAF        (waf_engine.lua)
gateway_resp.lua       (access phase)   status codes, backend selection
header_filter          ApiGw.header_filter()  correlation / RateLimit-* headers
log                    ApiGw.log()            structured audit line

Within api_gw the stages run in a fixed order. The order is declared once in api/api_gw/pipeline.lua, and the docs and tests read it from the same source. The first stage to return a decision ends the pipeline. A request rejected by IVT therefore never reaches the auth verifier, and a CORS preflight never uses up rate-limit quota.

WSLProxy Edge api_gw pipeline: real IP, correlation, IVT guard, request hygiene, rate limit and audit in front of the Payments Secure API

PriorityStageWhy it sits there
1000real_ipEverything downstream keys on the client IP
995hooks_requestDeclarative header transforms and optional Lua hooks
980correlationSet before anything can deny, so every rejection carries an ID
950corsPreflights terminate here, unauthenticated and unmetered
900ivtCheap structural checks before parsing or verifying anything
850request_securityContent-Type, body size and token shape on traffic judged real
800authOptional credential verification (none, passthrough, jwt, api_key)
700rate_limitLast, so a quota can key on a verified consumer

4. The configuration, module by module

Everything below lives in one file in the app repository, .github/wslproxy/data/servers/prod/host:int-api.payments-secure.fictionally.org.json, under the api_gw key. The enabled modules are:

"api_gw": {
  "enabled": true,
  "tenant_id": "payments-secure-int-api",
  "modules": ["real_ip", "request_security", "ivt", "rate_limit", "audit"],
  ...
}

An explicit tenant_id gives the environment its own counters and audit identity. By default the tenant is <profile>/<server_name>, so the same hostname in two environments never shares a quota.

4.1 real_ip: believe forwarded headers only from trusted hops

"real_ip": {
  "trusted_cidrs": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
                    "<the CDN's published edge ranges>"],
  "recursive": true
}

The client IP is taken from the forwarded header only when the directly connected peer falls inside a trusted range. recursive walks back through the chain of trusted hops to the first untrusted address. That address is the real client. A request sent straight to the origin with a forged X-Forwarded-For is judged by its actual socket address. Without this stage, every per-IP limit further down could be bypassed with one header.

4.2 request_security: correlation, Content-Type and body size

"request_security": {
  "correlation":  { "enabled": true, "header": "X-Correlation-ID" },
  "content_type": {
    "enforce": true,
    "methods": ["POST", "PUT", "PATCH"],
    "allow": ["application/json", "multipart/form-data",
              "application/x-www-form-urlencoded"],
    "exempt_paths": ["/health", "/ready", "/opsapi/health", "/fastapi/health"]
  },
  "max_body_bytes": 10485760
}
  • Correlation. An incoming X-Correlation-ID is kept. If there is none, one is minted and forwarded to OpsAPI and FastAPI. The ID is also returned to the browser and written to the audit line, so a support ticket can be traced across the edge and both services.
  • Content-Type. Writes must declare one of three types the app actually accepts. A text/xml or missing type on a POST is refused before any parser sees it.
  • Body size. 10 MB is the global ceiling, sized for statement PDFs, CSVs and supporting documents. Routes tighten it further (see section 5).

4.3 ivt: invalid-traffic guard, in audit mode

"ivt": {
  "mode": "audit",
  "methods": { "deny": ["TRACE", "TRACK", "CONNECT"] },
  "path_denylist": ["/wp-admin", "/wp-login", "\\.php$", "/\\.env$", "/\\.git/"],
  "require_auth_shape": true,
  "strip_header_prefixes": ["X-Internal-", "X-Payments-Internal-",
                            "X-Consumer-", "X-Authenticated-"],
  "burst": { "window_seconds": 10, "max_requests": 200, "key": "ip" },
  "block_threshold": 5
}

IVT scores each request against cheap structural signals: methods no browser sends, paths that do not exist on our stack, an Authorization header that is not even shaped like a bearer token, and more than 200 requests in 10 seconds from one client. Header prefixes that only our own components may set are stripped from the inbound request, so a backend never sees a client-supplied X-Authenticated-User. A score at or above block_threshold becomes a rejection in block mode.

We deployed IVT in audit mode. In audit mode every finding is recorded in the audit line and the request continues. monitor mode also adds an X-WSL-IVT: <verdict>;score=<n> response header, and only block mode rejects. The documented path is audit, then monitor, then block. Header-spoof stripping is the exception: it applies in every mode except disabled, including audit, because removing a header a client should never have been able to set cannot hurt a real user. Starting in audit mode lets us measure the false-positive rate on real traffic before IVT is allowed to say no.

4.4 rate_limit: named profiles, sliding window

"rate_limit": {
  "enabled": true,
  "algorithm": "sliding",
  "default_profile": "standard",
  "profiles": {
    "health":    { "limit": 0 },
    "auth":      { "limit": 60,  "window_seconds": 60, "key": "ip" },
    "public":    { "limit": 120, "window_seconds": 60, "key": "ip" },
    "standard":  { "limit": 600, "window_seconds": 60, "key": "ip" },
    "expensive": { "limit": 30,  "window_seconds": 60, "key": "ip" },
    "upload":    { "limit": 60,  "window_seconds": 60, "key": "ip" }
  }
}

Profiles are named by the kind of work a route does, not by its URL, so the routes table stays readable. limit: 0 means unmetered: probes from Kubernetes and uptime monitors must never be throttled into a false outage. A sliding window avoids the boundary burst of a fixed window, where a client can send twice its allowance across the change of a minute. Responses carry RateLimit-Limit and RateLimit-Remaining, so well-behaved clients can slow down before they are refused.

The auth profile started at 20 requests per minute and was raised to 60 after the first day's incident (see section 8). A login page makes several calls per attempt, and people behind a shared office NAT share an IP.

4.5 audit: one redacted line per request

"audit": {
  "enabled": true,
  "level": "info",
  "sample_rate": 1,
  "include_headers": ["user-agent", "referer"]
}

Every request emits one structured JSON line: tenant, route name, stage decisions, IVT score and reasons, rate-limit result and correlation ID. The lines feed the WSLProxy Grafana dashboards. Only the two named headers are included, and sensitive headers such as Authorization are always redacted. Client identities (IP, API key, JWT subject) are written as tenant-salted SHA-256 digests, never raw, so the same client hashes differently for every tenant. For an app handling payment and account data, this is how the security log avoids becoming a second store of personal data.

5. The route map

Routes are matched in order, most specific first. Each route picks a rate profile, an auth mode and, optionally, a tighter body limit or an IVT override.

RoutePathRate profileBody capNotes
health, ready/health, /ready (equals)health (unmetered)n/aIVT disabled
opsapi-health, fastapi-health/opsapi/health, /fastapi/healthhealthn/aIVT disabled
google-oauth, google-oauth-root/opsapi/auth/google, /auth/googleauthglobalIVT disabled: third-party redirects carry unusual shapes
opsapi-auth, auth/opsapi/auth, /authauth (60/min)64 KBLogin and registration bodies are small
uploads/fastapi/api/v1/statementsupload (60/min)10 MBSupporting documents
fastapi/fastapistandard (600/min)globalAuth passthrough to the service
opsapi/opsapistandard (600/min)globalAuth passthrough to the service
catchall/public (120/min)globalAnything unexpected gets the tightest general limit

Auth is passthrough on application routes: the edge checks that a token is shaped like one, and the services, which own the session model, verify it. WSLProxy can verify JWTs or API keys at the edge. We keep that for a later step so that one change does not alter two security boundaries at once.

6. Isolation and failure behaviour

Every shared-memory key is built through one function: agw:1:<tenant>:<module>:<part>…. Colons and percent signs in any component are escaped, so a crafted header value cannot cross into another tenant's counters. That matters because the same edge also fronts other Workstation and customer domains.

The gateway's failure behaviour is deliberate and written down:

  • Counters fail open. If the shared dictionary is missing or full, or a stage throws, the error is logged, the audit line records unavailable, and traffic flows. A broken rate limiter must not take the payments API offline at peak.
  • Authentication fails closed. If the edge is configured to verify a credential and cannot resolve its key, it returns 401. An edge that cannot verify must not pretend it did.
  • Unknown profile names referenced by a route are not limited and are flagged as unknown_profile, so a typo is visible in the logs instead of silently blocking a route.

7. How we rolled it out

  1. Declare the front door. We first tagged the integration API's origin rules as served by the WSLProxy API gateway and set the gateway header, so the config itself records which component owns edge policy.
  2. Commit the policy. The api_gw block went into the app repository as a normal pull request. On merge, the WSLProxy register workflow, which runs on our self-hosted runners, pushes the changed server record to the London edge through the admin API. No SSH and no nginx reload.
  3. Integration first, audit mode on. INT gets real daily use from the team and testers without exposing customers to a mistake.
  4. Watch the dashboards. IVT would-be blocks, rate-limit headroom per profile and 4xx rates per route are all visible in Grafana from the audit stream.

Because the git file is the source of truth, the register workflow also stops a later sync from quietly reverting a live change made through the admin UI. Commits to the file are the change log.

8. The incident: CORS at the edge broke password login

The first version of the policy also enabled the cors module. It had a tight allowlist of request headers: Authorization, Content-Type, X-Correlation-ID, X-Namespace-Id and X-Request-Id. Because CORS runs at priority 950, the edge answered browser preflights itself, before they reached OpsAPI.

The INT front end sends an X-Project-Code header on password login. It was not on the list, so the browser's preflight came back without it in Access-Control-Allow-Headers. The browser then correctly refused to send the real POST, and password login failed. Google OAuth also passed through the new auth-profile limits, and at 20 requests per minute that was tighter than a real sign-in flow needs.

The fix shipped three minutes after the enabling commit:

  • Hand CORS back to the application. cors was removed from modules. OpsAPI (Lapis) already owned a complete CORS policy, including mandatory fraud-prevention / device-intelligence headers (X-Gov-Client-Device-ID, X-Gov-Client-Screens, X-Gov-Client-Timezone and the rest). The edge policy has been widened to the full header set for the day it is re-enabled.
  • Raise the auth profile from 20 to 60 requests per minute, and the auth body cap from 8 KB to 64 KB.
  • Add explicit Google OAuth routes with IVT disabled, because OAuth redirects legitimately carry query shapes and referrers that structural heuristics may flag.

Three lessons we took from it:

  1. Only one layer should own CORS. When both the edge and the app answer preflights, the stricter one wins silently, and the error appears in the browser console, not in either set of logs. Pick one owner and document it.
  2. Build the header allowlist from real traffic, not from memory. A regulated fintech app sends many custom headers. The audit stream records them, so read it first.
  3. Staged rollout is what made this a three-minute fix. The mistake happened in INT, and reverting it was one small reviewed commit.

9. What we have now

  • One policy for two services. OpsAPI and FastAPI get identical IP handling, request hygiene and throttling without middleware in either codebase.
  • Scanner traffic is visible. Every .env, .git, WordPress and PHP probe is tagged with reasons and a score, ready to block when we switch modes.
  • Login is throttled. Password spraying from a single address hits the auth profile's per-minute ceiling instead of the password hash function.
  • Spoofed internal headers never reach the app, and X-Forwarded-For is only believed from trusted hops.
  • End-to-end traceability. Every request has a correlation ID that appears in the browser response, the edge audit line and the service logs.
  • No new infrastructure. No Kong cluster, no gateway database and no extra hop. The change was a JSON block reviewed like code.

10. Next steps

  1. Move IVT from audit to block on INT once the would-be-block log shows no genuine customer traffic.
  2. Promote the same block to the production API through the same pull-request flow.
  3. Add an expensive route for extraction and calculation endpoints, which today share the standard profile.
  4. Consider edge JWT verification for FastAPI routes, which lets the edge rate-limit per verified user rather than per IP.

11. A checklist for your own API

  • Set real_ip.trusted_cidrs to your CDN and internal ranges before relying on any per-IP control.
  • Turn on correlation IDs first. They cost nothing and make every later decision debuggable.
  • Name rate profiles by kind of work (auth, upload, standard) and leave health checks unmetered.
  • Put the tightest general limit on the catch-all route, not the loosest.
  • Strip internal-only header prefixes at the edge.
  • Start IVT in audit, and roll out to a non-customer environment first.
  • Decide which layer owns CORS, and only one.
  • Keep the policy in git and deploy it with the same pipeline as everything else.

12. FAQ

Is WSLProxy really open source?

Yes. The source, including the api_gw package, its JSON schema, examples and Lua tests, is on GitHub. There is also a migration guide for teams moving from Kong.

Does this replace the WAF?

No. api_gw runs before the WSLProxy WAF engine in the same request. The gateway handles identity, hygiene and quotas. The WAF inspects request content for attack payloads.

What does it cost in latency?

The stages are shared-memory lookups and string checks in the nginx worker, with no network calls. Body inspection is left to the WAF so the body is not buffered twice.

Can I try it on my app?

Yes. The WSLProxy docs describe enabling a new tenant in under ten minutes, and we can help if you would like it run for you.

Workstation designs, implements and maintains API-gateway AI-agent protection for fintech applications. One of the apps we protect in this way sits in the portfolio of a large AI fintech — cognoaiuk.com.

分享這篇文章

更多 Technology 內容

AMD Instinct vs NVIDIA for AI Infrastructure: Evaluate, Port and Run Both (2026)

AMD Instinct vs NVIDIA for AI Infrastructure: Evaluate, Port and Run Both (2026)

From Lisa Su's 2024 interview to Helios: the 2026 platforms layer by layer, Instinct product lines, memory arithmetic, the CUDA-to-ROCm software map, a porting workflow with commands, dual-vendor Kubernetes, fair benchmarking, a decision workflow and day-two operations

閱讀更多
Private AI for Financial Data: Residency, Routing and Cost on AWS Bedrock, Microsoft Azure and Your Own GPUs

Private AI for Financial Data: Residency, Routing and Cost on AWS Bedrock, Microsoft Azure and Your Own GPUs

Billing models and idle cost, per-call cost at three volumes, Bedrock in-region vs EU profiles, Converse API with prompt caching and IAM region enforcement, Azure deployment types and Claude on Foundry, UK and EU providers, a residency-aware provider registry, cost-cutting workflow, a golden-set evaluation harness and GPU break-even

閱讀更多
NVKE and NVKR Technote: Building a Managed GPU Kubernetes Service from Bare Metal, with Reproducible, Verifiable Builds

NVKE and NVKR Technote: Building a Managed GPU Kubernetes Service from Bare Metal, with Reproducible, Verifiable Builds

Architecture, components and pinned versions, the NVKR Definition-Run-Release-Verify lifecycle, the full nvkectl workflow from racked servers to a customer cluster, node attestation, day-two operations, every preview limitation with a mitigation, a comparison with other approaches and a staged evaluation plan

閱讀更多