Your proxy asks. We answer in milliseconds.
A policy enforcement point beside your proxy, on the same point of presence as the handshake. Your F5, NGINX, Envoy or Kong keeps the TLS handshake and sends us the certificate it saw: one sideband call per new certificate, from the iRule, the Lua or the auth filter you already have. Your traffic path never moves.
200 today. 403 when you say so.
In observe every answer is 200: the verdict rides along as headers and a signed assertion, your own checks stay in charge. In enforce a QWAC that fails gets 403 from us and your proxy returns it. One switch per hostname, on your account page, in effect on the next request. Every decision leaves a signed, sealed and time-stamped record: the duly evidenced reason PSD2 Article 68 asks for when you refuse, ready for the PSR.
Under your own name. Only your proxy can call it.
A hostname on our zone, picked by you, or a name of your own: one TXT record proves it is yours, one CNAME brings it here, and the certificate for it is issued and renewed by us, never one for you to manage. Your proxy presents a bearer minted once, or its own client certificate, registered by fingerprint. Nothing else is answered.
- 1rule in your proxy
- 200 · 403the two answers
- 3 msengine, warm
- 0changes to your traffic path
- ES256signed verdict, your key
- EUone object per account
G1 · Observe or enforce
Second opinion today. Gatekeeper when you say so.
The Gatekeeper is a policy enforcement point at the edge: an edge-native, multi-tenant mTLS mesh beside your proxy, under your own names, answering on the same point of presence where the handshake happened. Your proxy keeps the TLS handshake and your traffic keeps its path. On a new certificate it makes one sideband call to us, from the iRule, the Lua or the auth filter you already have, and we answer in milliseconds. In observe the answer is always 200: the verdict rides along as headers and a signed assertion, your own checks stay in charge, ours are a second opinion. In enforce a QWAC that fails gets 403 from us and your proxy returns it. You flip the switch per hostname from your account page. It applies on the next request. No ticket, no redeploy, no DNS. Call it by your own name and the certificate for that name is ours to issue and renew: no certificate for you to manage, ever. Where that sits in your own drawing, in two views:
Observe
We always answer 200. The verdict travels on to your gateway as one header and one signed assertion; it reads them, logs them, compares them with its own truststore. Nothing is refused on our account. Start here: a second opinion with a full audit trail.
Enforce
A QWAC that is not qualified, not in a trusted list, revoked, or whose licence is withdrawn gets 403 from us, and your proxy returns it. Your API only ever sees traffic that passed.
The switch is yours
Per hostname, from your account page, in effect on the next request. Run a pilot in observe, read the headers for a week, then flip. Flip back just as fast. Your proxy rule never changes.
G2 · Four ways in
Your live traffic stays where it is.
No bank re-routes production traffic for a certificate check, and we do not ask you to. The Gatekeeper is something your proxy calls, something you run in your own edge, or something the TPP talks to before it reaches you. The only time we stand in a path is a sandbox you put us in. Same verdict, same headers, same switch.
Keep your proxy. Ask us.
Your F5, Citrix, NGINX, Envoy or Kong keeps the TLS handshake and sends us the certificate it saw: one rule, one call per new certificate. We answer in milliseconds with the verdict in headers and a signed assertion, or 403 when you have switched to enforce. Prefer to call it by your own name? One TXT record proves gk.bank.example is yours, one CNAME to connect.tppv.app brings it here, and the certificate for it is issued and renewed on our edge (Cloudflare for SaaS): you manage no certificate at all, and nothing expires on your watch. Your proxy authenticates with a bearer minted on your account page, or, on your name under tppv.app, with its own client certificate in the handshake. Nothing in your traffic path either way.
POST https://nordbank.tppv.app/_authz # or https://gk.bank.example/_authz
Authorization: Bearer tppv_sb_… # or none: your proxy's client certificate in the handshake
X-SSL-Client-Cert: <base64 DER>
HTTP/2 200 · X-QWAC-Valid: true · X-QWAC-Assertion: eyJ…Envoy, NGINX, F5 and the raw contractThe engine in your own edge
The same engine, deployed in your Cloudflare account, your VPC or an air gap, with the trusted lists ingested on your side. Speaks the sideband contract over HTTP, and ICAP REQMOD through a small adapter you run in your DMZ. Nothing of yours crosses the internet.
# the engine, deployed in your own Cloudflare account, VPC or DMZ
tppv-icap --listen :1344 \
--upstream http://gatekeeper.internal/_authzLicensed with MSA and DPAThe TPP asks us first
For an OAuth-shaped flow: the TPP does mTLS against auth.nordbank.tppv.app, we hold that handshake, verify the QWAC and issue a short-lived JWT bound to the certificate, RFC 8705 style. The TPP carries it to your API, and your API verifies one more JWT. Your traffic path is untouched.
POST https://auth.nordbank.tppv.app/ticket (mTLS)
→ { "access_token": "eyJhbGciOiJFUzI1NiIs…", "expires_in": 60 }
GET https://api.bank.example/psd2/v1/accounts
Authorization: Bearer eyJhbGciOiJFUzI1NiIs…What the token saysA path you choose to put us in
For a test environment or a pilot, a hostname on our zone can sit in front of your sandbox API: TPPs connect to it, we hold the handshake, and forward with the verdict attached. It is the one place we are in a path, and only because you put us there. Not for production, not by DNS.
https://nordbank.tppv.app/sandbox/v1/accounts
X-QWAC-Valid: true
X-QWAC-Roles: PSP_AI,PSP_PI
X-QWAC-Assertion: eyJhbGciOiJFUzI1NiIs…Pick the hostname on your account pageG3 · Side by side
What changes, and what does not.
| Way in | You change | Who holds the handshake | Can block | Added time | The verdict arrives as |
|---|---|---|---|---|---|
| Sideband | One rule in the proxy you run. Optionally one TXT record and one CNAME so the call goes to your own name. | You do. We see the certificate it saw. | 403 | One sideband call per new certificate. | 200 or 403, headers and the assertion in the answer. |
| Self-hosted | A deploy in your account or your DMZ. | You do, or your copy of us. | 403 | Nothing crosses the internet. | The same contract, on your network. |
| Ticket | Your API verifies one more JWT. The TPP adds one call. | We do, with the TPP, before it reaches you. | JWT | One call for the TPP. Nothing in your path. | A certificate-bound JWT the TPP carries. |
| Sandbox on our zone | A base URL in your sandbox documentation. | We do, in front of your test API. | Enforce | One edge hop. | Headers and the assertion on every request. |
Observe never blocks, whichever way in. Enforce is one switch on your account page, per hostname. None of the four touch your production traffic path.
| Where the verdict is applied | Edge PEP · sideband | ICAP · adapter |
|---|---|---|
| Where it runs | On our edge, the same point of presence where the handshake happened. Multi-tenant, under your own name. | In your DMZ, next to the load balancer, as a container you host. |
| What you change | One rule in the proxy or API gateway you already run. | An ICAP profile on the load balancer, and the adapter beside it. |
| Added time | Milliseconds: one HTTPS call per new certificate. | One more hop inside your network, then the same call onward. |
| Fits | Any proxy or gateway that can make a sideband call: that is nearly all of them. | Load balancers whose policy only speaks ICAP. |
| The contract | 200 or 403, X-QWAC-* headers and the signed assertion. | The same verdict, the same headers: the adapter speaks our sideband contract onward. |
A policy enforcement point is where the verdict is applied. Ours is an edge-native, multi-tenant mTLS mesh beside your proxy, under your own names. ICAP is the same decision over an older transport, through an adapter you host.
G4 · Sideband, in your proxy's own words
One rule in the proxy you already run.
The sideband endpoint answers the way a proxy listens: 200 lets the request through, 403 stops it, nothing else. That is why a failed QWAC is 403 in enforce and 200 with the verdict attached in observe, while API v6, which answers a validation question, says 422. Same engine, same reason codes. The endpoint takes the certificate exactly as your proxy saw it and answers only to your proxy: the sideband bearer minted on your account page, or the client certificate you registered there by fingerprint. Anything else gets 401, on a hostname no TPP has a reason to call. That is what makes a forwarded header trustworthy: not the header, the caller. It is the same offload you already do with an iRule, a Lua hook or an auth filter. Pick your proxy.
curl -si -X POST "https://nordbank.tppv.app/_authz" \
-H "Authorization: Bearer $TPPV_BEARER" \
-H "X-SSL-Client-Cert: $(openssl x509 -in qwac.pem -outform DER | base64 -w0)"
# the bearer (tppv_sb_…) is minted on your account page and shown once; we keep its hash.
# or no bearer at all: --cert proxy.pem --key proxy.key, the proxy certificate registered
# by its SHA-256 fingerprint on the account page, two at most so one can rotate.
# On a name of your own by CNAME the bearer is what applies.
HTTP/2 200 # 403 in enforce when the QWAC fails
X-QWAC-Valid: true # the one your proxy branches on
X-QWAC-Reason: # only when false: why
X-QWAC-Assertion: eyJhbGciOiJFUzI1NiIsImtpZCI6Im5iLTIwMjYtMTAifQ… # the one your backend verifies
# three headers are the contract. Optional, switched on per account when a gateway wants them:
# X-QWAC-Roles, X-QWAC-Org-Id, X-QWAC-Country, X-QWAC-TSP, X-TPP-Passports
# also accepted on the way in: X-Forwarded-Client-Cert (Envoy XFCC) or a PEM body
# listener: forward the client certificate to the filter chain
forward_client_cert_details: APPEND_FORWARD
set_current_client_cert_details: { cert: true, chain: true }
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
http_service:
server_uri: { uri: https://nordbank.tppv.app, cluster: tppv, timeout: 0.5s }
path_prefix: /_authz
authorization_request:
allowed_headers: { patterns: [ { exact: x-forwarded-client-cert } ] }
headers_to_add: [ { key: authorization, value: "Bearer %TPPV_BEARER%" } ]
authorization_response:
allowed_upstream_headers: { patterns: [ { prefix: x-qwac- }, { prefix: x-tpp- } ] }
failure_mode_allow: true # observe: never fail closed on us
location = /_qwac {
internal;
proxy_pass https://nordbank.tppv.app/_authz;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Authorization "Bearer $tppv_bearer";
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
}
location /psd2/ {
auth_request /_qwac; # 2xx passes, 403 stops
auth_request_set $qwac_valid $upstream_http_x_qwac_valid;
auth_request_set $qwac_jwt $upstream_http_x_qwac_assertion;
proxy_set_header X-QWAC-Valid $qwac_valid;
proxy_set_header X-QWAC-Assertion $qwac_jwt;
proxy_pass https://psd2_backend;
}
# iRule sideband to a virtual server that carries a server-SSL profile
# and our hostname. APM's HTTP Connector does the same without an iRule.
when HTTP_REQUEST {
set cert [b64encode [SSL::cert 0]]
set conn [connect -protocol TCP -timeout 500 tppv_sideband_vs]
send -timeout 500 $conn "POST /_authz HTTP/1.1\r\nHost: nordbank.tppv.app\r\n\
Authorization: Bearer $static::tppv_bearer\r\nX-SSL-Client-Cert: $cert\r\n\
Content-Length: 0\r\nConnection: close\r\n\r\n"
set resp [recv -timeout 500 $conn]
close $conn
if { [string match "HTTP/1.? 403*" $resp] } {
HTTP::respond 403 content "QWAC not accepted" "Content-Type" "text/plain"
return
}
regexp -nocase {x-qwac-assertion:\s*(\S+)} $resp -> jwt
HTTP::header insert X-QWAC-Assertion $jwt
}
Citrix ADC an HTTP Callout to nordbank.tppv.app/_authz with
CLIENT.SSL.CLIENT_CERT in X-SSL-Client-Cert, and a
responder policy on the callout's status for enforce.
Kong the Request Transformer adds the certificate header
from the mTLS plugin; a custom auth plugin or
the Forward Auth plugin calls /_authz.
Apigee a ServiceCallout policy with the client certificate,
then a RaiseFault on 403.
HAProxy http-request lua-request or a SPOE agent; or run the
Gatekeeper self-hosted next to it and skip the hop.
Anything the contract on the first tab. One POST, one answer.
G5 · One verdict, signed
Headers for your gateway. A signature for your auditors.
Our answer reaches your proxy over TLS with nothing in between: there, a header is ours. It is the hops after that, inside your own perimeter, where a header is just a header. So every verdict also travels as a compact JWT, signed with a key that belongs to your account alone. Your API verifies it against a JWKS on your own hostname, the way it already verifies any token, and years later it still proves what we saw. Headers for routing, the assertion for trust.
What the token says
Issued per request, short-lived, bound to the certificate's fingerprint. The claims are the verdict and its sources: the trusted list and its sequence, the revocation answer, the register record's hash. An auditor traces it from the token alone; the audit on your account page does exactly that.
{
"iss": "https://nordbank.tppv.app", // your own name once it is live
"aud": "psd2-api",
"iat": 1792742400, "exp": 1792742460,
"jti": "01JB…", "sub": "PSDSE-FINA-556000-0000",
"qwac": { "valid": true, "indication": "PASSED", "type": "QWAC",
"orgId": "PSDSE-FINA-556000-0000", "country": "SE", "serial": "4f3a…",
"roles": ["PSP_AI","PSP_PI"], "tsp": "Netcetera Software Services GmbH",
"register": true, "passports": ["SE","DK"] },
"tl": { "territory": "SE", "seq": 142 },
"rev": { "status": "good", "source": "ocsp", "at": "…", "until": "…" },
"reg": { "ok": true, "hash": "7c1f…", "at": "…", "passports": ["SE","DK"] },
"cnf": { "x5t#S256": "9f2c…" }
}Any gateway that reads a JWKS
Point your JWT filter at the JWKS on your hostname. When you rotate the key from your account page, your gateway picks the new one up by itself.
# Envoy jwt_authn
providers:
tppv:
issuer: https://nordbank.tppv.app
audiences: [ psd2-api ]
remote_jwks:
http_uri: { uri: https://nordbank.tppv.app/.well-known/jwks.json, cluster: tppv, timeout: 1s }
cache_duration: 300s
from_headers: [ { name: X-QWAC-Assertion } ]Audience, lifetime and rotation on your account pageAsymmetric only, one object per account
ES256 on P-256. We never hand out a shared secret: the private key is made and kept in a store that belongs to your account alone, in the EU, and signs without ever leaving it. Rotate from your account page; the previous key stays in the JWKS for an hour, so a gateway that cached it keeps verifying. Nothing you store can mint a verdict, and nothing of yours sits in a table next to another bank's.
The same token as a ticket
In the ticket flow the TPP fetches exactly this JWT from auth.nordbank.tppv.app after an mTLS handshake with us, and carries it to your API. Your API verifies the token and the certificate fingerprint in cnf. The same key, the same JWKS.
G6 · From your account
Your settings, not our tickets.
Everything the Gatekeeper does for you is set on your account page, by you, in effect on the next request.
Mode, per hostname
Observe or enforce. The one switch this page is about.
Your hostname
Your name on tppv.app, picked by you, with a ping that shows it answering. It is where your proxy calls, where your JWKS lives, where the TPP fetches a ticket, and where a sandbox can sit. Never your production traffic path.
Your own name
A subdomain you govern, gk.bank.example, in three steps you watch on the page: a TXT record at _tppv.gk.bank.example with the token we show you, which proves the name is yours before anyone can claim it; a CNAME to connect.tppv.app; and the certificate we issue on it. Once both are active, the JWKS and the issuer of every signed verdict move to your name.
Sideband credential
What your proxy presents, whichever name it calls. A bearer, minted once and shown once, kept by us only as its hash; or your proxy's own client certificate, registered by SHA-256 fingerprint or pasted as PEM, two at most so one can rotate. With the certificate in the handshake the call needs no bearer. One is enough; both stay valid if you set both.
Origin and passports
Where we forward, and which jurisdictions the licence must cover. The register check runs against exactly those.
Signed verdict
Audience, lifetime, the JWKS URL to paste, the current kid, and a rotate button.
Audit
Paste an assertion from your gateway log, or a v6 answer. The signature is checked against your key, and the verdict is traced to the trusted list, the revocation answer and the EBA record, stamped, and printed as a sheet.
G7 · Plain facts