A Content Security Policy is stored per host of a CID. The page that edits it is Content Security Policy.
Saving or deleting a host policy stores the previous policies of the CID. The backup methods are get_csp_settings, set_csp_settings, del_csp_settings, diff_csp_settings and restore_csp_settings under /rollback/v1/. They are Enterprise only, the same as these methods. See Backup.
The methods are available on the Enterprise plan. A Business key receives 401 and {"status": "fail", "description": "Not available for your plan"}. Below Enterprise, get_global and get_server omit the policy, and get_global_hash and get_server_hash do not follow it, so Filter does not install the header. If a location still proxies reports to 127.0.0.1:5105, Filter does not forward that request. MLA does not listen for reports below Enterprise. The general conventions are in API methods.
state is one of off, learning, report_only and enforce.
off sends no header.learning sends Content-Security-Policy-Report-Only with default-src 'none', so the browser reports every resource and blocks nothing.report_only sends the approved policy in Content-Security-Policy-Report-Only.enforce sends the approved policy in Content-Security-Policy.directives is an object of directive name to a list of source expressions. The names are base-uri, connect-src, default-src, font-src, form-action, frame-ancestors, frame-src, img-src, manifest-src, media-src, object-src, script-src, style-src and worker-src. A source is a keyword such as 'self', a scheme such as https:, or a host. A value that could close an Nginx directive is rejected.
options carries nonce, report, report_path and retention_days. The defaults are nonce: false, report: false, report_path: /__mikiri/csp-report and retention_days: 30.
With nonce: true the script-src allowlist is replaced by 'nonce-$mikiri_waf_csp_nonce', 'strict-dynamic', https: and 'unsafe-inline'. Nginx passes the same nonce to the application in the X-CSP-Nonce request header the application puts it on its own script tags.
With report: true the policy ends with report-uri and the path from report_path. The browser posts the report to that path on the same host. The agent beside Nginx keeps the host, the directive, the blocked origin and a generic page path drops the referrer, the query string, the script sample and the visitor address.
Saving a policy does not edit the origin. Filter applies it.
The agent on Filter polls get_global_hash and get_server_hash. Both hashes follow the policy text that is written into the configuration, so a change to that text makes the agent download the Nginx configuration again and reload Nginx. A row that is not rendered does not move these hashes: a host left off, the inventory and the timestamp. Until that reload, responses still carry the previous header.
get_global still emits the map when the CID has no global Nginx block. get_global_hash then returns a hash of that text instead of {"status": "none"}. The response is none again when nothing is rendered, and Filter downloads the empty file and drops the old map.
The policy text is an http-level map, not a copy inside each server block. Two maps key on 1:$host:
$mikiri_waf_csp holds the policy of a host in enforce. The header is Content-Security-Policy.$mikiri_waf_csp_ro holds the policy of a host in learning or report_only. The header is Content-Security-Policy-Report-Only.A third map sets $mikiri_waf_csp_html to 1 only when the response Content-Type is text/html. The policy maps match 1:$host, so a stylesheet, a script or an image does not receive the header. An empty map value is omitted Nginx does not send an empty header.
When at least one host has a policy, every server block every location that already has its own add_header, contains:
add_header Content-Security-Policy $mikiri_waf_csp always;
add_header Content-Security-Policy-Report-Only $mikiri_waf_csp_ro always;
The lines are omitted when nothing is rendered. Nginx would not know $mikiri_waf_csp and $mikiri_waf_csp_ro nginx -t would fail. always keeps the header on an error response. A location that sets add_header of its own would otherwise stop inheriting the server lines, so the two lines are repeated there. The header the browser actually receives is the one whose variable is not empty.
$host is the name the browser requested. The row shop.example.com matches that name and does not match a URL, a scheme or a port. A host with active: false, state off, or a policy with no directive is left out of the map, so that name receives no header.
The header is added only on Filter. A request that reaches the origin without passing through Filter does not get it the browser does not apply this policy.
In nonce mode a map copies the per-request value $waf_csp_nonce into $mikiri_waf_csp_nonce for the hosts that use it. A proxy_pass location sends that value to the application as X-CSP-Nonce. For every other host the variable is empty Nginx does not pass an empty request header. The application has to put the nonce on its own script tags. A tag without it does not run.
When report is on and the state is not off, the server block gains an exact location for report_path. It accepts only POST, limits the body to 32 KB, rate-limits the client, drops cookies proxies to the agent on Filter at 127.0.0.1:5105. The report does not leave Filter. The counters come back through set_csp_reports and fill the inventory. They do not change the header. A source reaches the header only after it is written into directives and Filter reloads.
Request example:
curl http://api.example.com:8080/csp/v1/get_csp_settings --header 'Content-type: application/json' --data '{"key": "%PRODUCT KEY%"}'
Response example:
{
"status": "success",
"hosts": [
{
"host": "shop.example.com",
"active": true,
"state": "report_only",
"comment": null,
"directives": {"default-src": ["'self'"], "img-src": ["'self'", "https://cdn.example.com"]},
"inventory": {},
"options": {"report": true, "report_path": "/__mikiri/csp-report", "retention_days": 30},
"timestamp": "2026-09-30T12:00:00",
"policy": "default-src 'self'; img-src 'self' https://cdn.example.com; report-uri /__mikiri/csp-report",
"header": "Content-Security-Policy-Report-Only"
}
]
}
policy is the header value the browser would receive header is the header name. Both are empty while the host is off or the policy has no directive. inventory is what the scanner and the browsers have seen and what is not in directives yet.
An empty CID returns {"status": "none"}.
/csp/v1/get_csp_settings_hash follows the hash methods and covers the stored rows. The Nginx global and server hashes follow the rendered policy instead. An agent refetches its configuration when that text changes does not refetch it because the inventory or the timestamp changed.
set writes one host. A host that does not exist yet is created. Only the keys present in set are changed. The method is /csp/v1/set_csp_settings.
{
"key": "%PRODUCT KEY%",
"set": {
"host": "shop.example.com",
"state": "report_only",
"directives": {"default-src": ["'self'"]},
"options": {"report": true, "nonce": false}
}
}
new_host renames that row. The policy and the inventory stay. A name that is already used returns {"status": "fail", "description": "The host \"...\" already exists"}.
{"key": "%PRODUCT KEY%", "set": {"host": "shop.example.com", "new_host": "www.shop.example.com"}}
del removes the host and its policy.
{"key": "%PRODUCT KEY%", "del": {"host": "shop.example.com"}}
A wrong host, state, directive or source returns {"status": "fail", "description": "Incorrect value for ..."}.
The scanner sends what it found in the pages. The method is /scanner/v1/set_csp_inventory.
{
"key": "%PRODUCT KEY%",
"csp": [
{
"host": "shop.example.com",
"sources": {
"img-src": [{"value": "https://cdn.example.com", "count": 3, "evidence": ["https://shop.example.com/"]}]
}
}
]
}
The response is {"status": "success", "updated": 1}. A source that is already in directives is not stored again. A source that has not been seen for retention_days is dropped. An entry without a timestamp is kept.
The agent on the protected host sends counters. The method is /csp/v1/set_csp_reports.
{
"key": "%PRODUCT KEY%",
"reports": [
{"host": "shop.example.com", "directive": "connect-src", "blocked": "https://api.example.com", "count": 4, "path": "/checkout"}
]
}
The response is {"status": "success", "updated": 1}. The same counters are merged into inventory, with origin set to report.