Skip to main content

Web Application Firewall

Configure a WAF, API gateway or reverse proxy so that wallets reach the protocol endpoints from anywhere while the management API stays closed to the internet. EUDIPLO has no built-in rate limiting or IP filtering, so this layer does both. TLS termination and general proxy settings are on TLS and reverse proxy.

Two surfaces on one host​

SurfacePathsCallersNetwork access
Management API/api/*Your backends, the web client in operators' browsers, the CLI, your configuration pipelineKnown networks
Protocol endpointsAll other pathsWallets, users' browsers, your pages that use the DC API, relying parties that read status and trust listsInternet

Match the /api/ prefix case-insensitively: the backend routes /API/session like /api/session, so a case-sensitive rule can be bypassed. If your WAF supports an allow list of paths, allow /api/, /.well-known/, /issuers/, /presentations/, /storage/, /health, /docs and /, and block the rest. Check the list against the path prefixes after each upgrade.

Management API​

Allow /api/* only from the addresses of your backends and operators, for example with an IP allow list, a VPN or an identity-aware proxy. Except for the token endpoint and the Swagger UI (/api/docs, /api/docs-json), every call also needs a bearer token, so the network rule is a second layer.

SettingValue
MethodsGET, POST, PUT, PATCH, DELETE, and OPTIONS for the CORS preflight of the web client
Content typesapplication/json; multipart/form-data for POST /api/storage, /api/config-bundles/plan/archive and /api/config-bundles/import/archive; application/x-www-form-urlencoded for POST /api/oauth2/token
Body size50 MB for the two archive routes, 10 MB for JSON
StreamingGET /api/session/{id}/events is a server-sent event stream: no buffering, long read timeout (TLS and reverse proxy)
Rate limitsPOST /api/oauth2/token, against guessing client secrets

Management request bodies contain values that injection signatures mistake for attacks: PEM keys and certificates (-----BEGIN …) in key chain imports, ${VAR} placeholders, JSON Schemas with regular expressions, DCQL queries, and the URLs of webhooks, attribute providers and trust lists. Once /api/* is limited to known networks, exclude the rules that fire there (see rules that break EUDIPLO) instead of loosening them for the whole host.

These routes outside /api serve operations rather than wallets:

RouteCallerRule
POST /issuers/{tenant}/authorize/interactive/complete-web-auth/{authSession}Your backend, with a management token (interactive authorization)Allow from your backends, like /api/*
GET /.well-known/oauth-authorization-server, GET /.well-known/jwks.jsonWeb client, CLI and OAuth libraries, to find the token endpointKeep open
GET /healthLoad balancer, orchestrator, eudiplo doctorMay be limited to internal networks
GET /docs, GET /docs-jsonPeople reading the protocol APIOptional; nothing depends on it

Protocol endpoints​

Wallets call these from phones on mobile networks, so:

  • Do not filter by IP address or country beyond what your use case allows.
  • Do not use browser challenges such as JavaScript challenges, CAPTCHAs or bot management interstitials. Wallets are HTTP clients; they cannot solve them, and the request fails.
  • Set per-IP rate limits high enough for many users behind one address; mobile networks share addresses through carrier-grade NAT.
  • Forward the path unchanged. DPoP proofs sign the method and URL, and the backend compares them with PUBLIC_URL plus the request path, so rewriting, case normalization or an added trailing slash breaks token and credential requests.
RouteMethodsRequest bodyCalled by
/.well-known/openid-credential-issuer/issuers/{tenant}, /.well-known/oauth-authorization-server/issuers/…, /.well-known/jwks.json/issuers/…GET–Wallets
/issuers/{tenant}/vci/credential-offers/{session}GET–Wallets
/issuers/{tenant}/vci/noncePOSTEmptyWallets
/issuers/{tenant}/vci/credentialPOSTJSON, or application/jwt when the wallet encrypts the requestWallets
/issuers/{tenant}/vci/deferred_credentialPOSTJSONWallets
/issuers/{tenant}/vci/notificationPOSTJSONWallets
/issuers/{tenant}/authorize/par, …/token, …/challenge, and par, token under chained-as/ and authorization-servers/{id}/POSTForm; challenge has no bodyWallets
/issuers/{tenant}/authorize/interactivePOSTForm or JSONWallets
/issuers/{tenant}/authorize, /issuers/{tenant}/chained-as/authorize, /issuers/{tenant}/authorization-servers/{id}/authorizeGET–User's browser, opened by the wallet
/issuers/{tenant}/chained-as/callback, /issuers/{tenant}/authorization-servers/{id}/vp-callbackGET–User's browser, redirected back by the upstream provider or wallet
/issuers/{tenant}/credentials-metadata/vct/{id}GET–Wallets, verifiers
/issuers/{tenant}/status-management/status-list/{id}, /issuers/{tenant}/status-management/status-list-aggregationGET–Wallets, verifiers
/issuers/{tenant}/trust-list/{id}GET–Verifiers, wallets
/presentations/{session}/oid4vp/request, /presentations/{session}/oid4vp/request/no-redirectGET, POSTPOST: form (wallet_metadata, wallet_nonce)Wallets
/presentations/{session}/oid4vpPOSTForm (response or vp_token) from wallets; JSON from your page (DC API)Wallets, your page
/presentations/{session}/iso-18013-7POSTJSON (data)Your page (DC API)
/storage/{key}GET–Wallets (logos, images)
SettingValue
MethodsGET, POST, and OPTIONS for the CORS preflight of pages that use the DC API
Content typesapplication/x-www-form-urlencoded, application/json, application/jwt
Body sizeAt least 100 KB. The backend accepts form bodies up to 100 KB and JSON bodies up to 10 MB. Presentations with several credentials and credential requests with several proofs or key attestations can exceed 8 KB.
HeadersDPoP, OAuth-Client-Attestation, OAuth-Client-Attestation-PoP and Authorization: DPoP … carry JWTs, several KB long with certificate chains (x5c). Keep header size limits at their defaults or above.
Rate limitsToken, PAR, nonce and credential endpoints

Rules that break EUDIPLO​

Wallet messages are base64url-encoded: JWTs, encrypted responses (JWE), SD-JWT presentations with ~-separated disclosures, and mdoc responses. Base64url uses - and _ as regular characters, and signatures, keys and ciphertext are random, so a token of a few kilobytes often contains --. Rules that look for SQL comments (--) or count special characters therefore block requests at random: the same wallet succeeds once and gets a 403 on the next attempt. Several wallet requests also carry JSON inside a form field, which SQL injection rules read as an attack.

OWASP CRS 4.30 with its default anomaly threshold blocked these requests without exclusions:

RequestParanoia level 1 (default)Added at paranoia level 2
Encrypted credential request (Content-Type: application/jwt)920420, every request–
Encrypted presentation response (JWE), from the wallet or your DC API page–942440 (--) in more than half of the requests; 942430
JSON in a form field: vp_token, wallet_metadata, authorization_details, openid4vp_response–942200, 942340, 942370, 942430
ISO 18013-7 response942100, rarely (1 of 400)942430
URLs of other hosts: redirect_uri in PAR and token requests, iss in the upstream callback–931130
PUT, PATCH, DELETE on /api911100–
Presentation configuration with a DCQL query (the field name dcql_query.credentials)930120–
Configuration with ${VAR} placeholders932130, 933135–
Key chain import with PEM blocks–942120, 942440
Credential configuration with JSON Schema patterns and URLs–931130, 932240, 942430

CRS skips values that look like a signed JWT (eyJ….eyJ….), so credential requests with JSON proofs passed. At paranoia level 3, PEM line breaks also trigger 920272. The managed rule sets of Azure WAF and the Cloudflare OWASP ruleset are derived from CRS, so expect similar matches there. With the AWS Core rule set, SizeRestrictions_BODY blocks bodies over 8 KB: presentation responses, credential requests with key attestations and configuration imports.

Whatever the product, run new rules in detection mode first (CRS: SecRuleEngine DetectionOnly, AWS: Count), go through issuance and presentation with your wallets several times, and add exclusions by rule and path from the WAF log.

OWASP CRS (ModSecurity, Coraza)​

Add these rules to REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf. Adjust the IDs if they collide with your own rules. With them, all requests of the table passed CRS 4.30 at paranoia levels 1 to 3.

# Management API: allow its methods and skip the injection rules that
# configuration bodies trigger. Restrict /api/ to known networks first.
SecRule REQUEST_FILENAME "@beginsWith /api/" \
"id:10010,phase:1,pass,nolog,t:none,t:lowercase,\
setvar:'tx.allowed_methods=GET HEAD POST PUT PATCH DELETE OPTIONS',\
ctl:ruleRemoveByTag=attack-sqli,\
ctl:ruleRemoveByTag=attack-rce,\
ctl:ruleRemoveByTag=attack-injection-php,\
ctl:ruleRemoveByTag=attack-lfi,\
ctl:ruleRemoveByTag=attack-rfi,\
ctl:ruleRemoveById=920272"

# Encrypted credential requests are sent as application/jwt.
SecRule REQUEST_FILENAME "@rx ^/issuers/[^/]+/vci/credential$" \
"id:10020,phase:1,pass,nolog,t:none,\
setvar:'tx.allowed_request_content_type=|application/json| |application/jwt|'"

# Wallet-facing endpoints: base64url tokens, JSON in form fields, redirect URIs.
SecRule REQUEST_FILENAME "@rx ^/(?:issuers|presentations)/" \
"id:10030,phase:1,pass,nolog,t:none,\
ctl:ruleRemoveByTag=attack-sqli,\
ctl:ruleRemoveById=931130"

On the wallet-facing paths only the SQL injection group and 931130 are removed: every value there is a token or identifier that the backend verifies, and its database queries are parameterized. The XSS, command injection, file access and protocol enforcement rules stay active. On /api/ the XSS and protocol enforcement rules stay active; the network restriction and the bearer token protect the rest.

AWS WAF​

With AWSManagedRulesCommonRuleSet:

  1. Override SizeRestrictions_BODY to Count. The rule then only adds the label awswaf:managed:aws:core-rule-set:SizeRestrictions_Body.
  2. Add a rule after the rule group that blocks requests with this label unless the URI path starts with /issuers/, /presentations/ or /api/.

AWS WAF inspects only the first 8 KB of a body behind an Application Load Balancer (16 KB, configurable up to 64 KB, on CloudFront and API Gateway); the signature rules continue with the inspected part of larger bodies.

Check the configuration​

From outside your known networks, the management API must be blocked in any spelling, and the metadata must be reachable:

curl -s -o /dev/null -w '%{http_code}\n' https://eudiplo.example.com/api/docs
curl -s -o /dev/null -w '%{http_code}\n' https://eudiplo.example.com/API/docs
curl -s -o /dev/null -w '%{http_code}\n' https://eudiplo.example.com/.well-known/openid-credential-issuer/issuers/<tenant>

The first two must not return 200; the third must. Then issue and present a credential several times with each wallet you support, through every flow you offer (QR code, same device, DC API), and look for blocked requests in the WAF log. Because the token false positives are random, one successful run is not enough. The load tests run many token requests, issuances and presentations against a deployment, which also exercises the WAF. Run eudiplo doctor from a network that may reach /api/* and /health.