What the Permissions-Policy Header Actually Does
You fixed the Content-Security-Policy warnings. You added Strict-Transport-Security with a two-year max-age. HTTP redirects to HTTPS on the first request, every time. Your grade climbed from a C to an A minus, and then it stalled.
The line item still holding you back reads something like "Permissions-Policy missing, minus 8 points." If you'd never heard of a permissions policy before opening that report, you're not alone. It's the header most people discover by accident, on their way to a near-perfect score, not on purpose.
That's a strange way to meet a security control, and it leaves a natural question unanswered: what does this header actually do, and why does a site with no camera, no microphone, and no interest in your location still lose points for not mentioning any of that? The short answer is that the permissions policy header isn't about what your site uses. It's about what your site, and every script running on it, is quietly allowed to ask the browser for. Left unset, that's a longer list than most site owners assume.
What Is the Permissions-Policy Header?
Permissions-Policy is an HTTP response header that lets a server tell the browser which of its powerful features, camera, microphone, geolocation, payment APIs, fullscreen, autoplay, and roughly two dozen others, a page and any iframes embedded inside it are permitted to use. It's set once, from the server, and it applies to the whole page load. Think of it as a manifest of hardware and sensor access, written by the site owner instead of assumed by the browser.
The name is intentionally literal. The header grants or removes permission to invoke specific browser APIs, and it does it before any script gets a chance to ask. If a directive is set to an empty allowlist, calling the matching JavaScript API (getUserMedia() for camera and microphone, the Geolocation API for location, and so on) fails immediately, regardless of what the script wanted or who wrote it. That last part matters more than it sounds: third-party ad tags, embedded widgets, and analytics snippets inherit the same restrictions as your own code, unless you specifically carve out an exception for them.
On securityheaders.tools, Permissions-Policy sits in the same reporting family as COOP, COEP, and CORP, the cross-origin isolation trio, because all four headers govern what a page and its embedded content are allowed to reach into. It carries real weight in the header checker's grade, and it's worth reading alongside the site's broader rundown of what each protective header trades off against usability, since Permissions-Policy is one of the few headers here that trades almost nothing. Turning off camera access on a blog costs you exactly zero functionality.
Here's the detail that catches people out: if you never send this header at all, you haven't opted out of anything. According to Chrome's own developer documentation, when Permissions-Policy is absent, the default allowlist for most features is effectively permissive for same-origin content, and cross-origin iframes fall back to whatever the page's allow attributes grant them (Chrome for Developers, Control browser features with Permissions Policy). No header doesn't mean no risk. It means unmanaged, which is a worse position to be in, not a neutral one.
What replaced Feature-Policy?
If you copied a header block from a tutorial written before roughly 2021, there's a decent chance it's using a header that no longer exists. Permissions-Policy replaced an earlier header called Feature-Policy, and the rename came with a full syntax overhaul, not just a new name on the same format.
Feature-Policy used space-separated tokens borrowed from CSP's style, wrapped in quotes:
Feature-Policy: geolocation 'self'; camera 'none'
Permissions-Policy switched to a structured-fields format, with parentheses instead of quoted tokens:
Permissions-Policy: geolocation=(self), camera=()
Same intent, different grammar. A browser parsing the old Feature-Policy syntax as if it were the new header simply won't recognize it and will ignore the directive entirely, which is the quiet failure mode: the header sits in your config, does nothing, and nobody notices until a grading tool points it out. Independent security researcher Scott Helme documented the switch as it happened, and MDN's reference page for the header spells out the current structured-fields syntax in full (MDN, Permissions-Policy header). If you inherited a config file from a previous developer, or from a Stack Overflow answer with a five-year-old timestamp, check the syntax before you trust it.
How Permissions-Policy actually works
Each directive in the header names a browser feature and pairs it with an allowlist describing who may use it. The allowlist is where the actual decision lives, and the W3C's Permissions Policy specification defines it as one of a handful of forms:
- () - an empty allowlist. The feature is disabled everywhere, including for your own page. No exceptions.
- self - the feature works for your own origin only. Content from other domains, including third-party iframes, is blocked.
- * - the feature is allowed for any origin, including every iframe on the page. This is rarely what you want; it's closer to leaving the door open than propping it.
- An explicit origin, such as
https://maps.example.com, allows that one named origin and no other.
A full header line strings several directives together, separated by commas:
Permissions-Policy: geolocation=(self), camera=(), microphone=(), payment=(), usb=(), fullscreen=(self)
If you've searched for a permissions-policy geolocation example specifically, that first directive is the one most sites end up writing: geolocation=(self) keeps your own site's mapping widget or store locator working while blocking every other origin from requesting the visitor's location through your page. Everything after it disables features the site has no legitimate use for. Removing any one of those directives doesn't disable the feature, it just leaves that feature back on the default, unmanaged setting described above.
Which features should you disable?
The full directive list runs past two dozen entries, and most guides stop at handing you all of them with no guidance on which ones apply to your site. That's not a useful place to stop. A blog and a SaaS dashboard have almost nothing in common in terms of what they need a browser to allow.
| Site type | Disable outright | Consider keeping open |
|---|---|---|
| Static or marketing site | camera, microphone, geolocation, payment, usb, bluetooth, midi, magnetometer | fullscreen=(self) if you embed video |
| SaaS or web app | usb, bluetooth, midi, nfc, unless the product genuinely uses them | geolocation=(self) for maps or location features, payment=(self) for checkout flows |
| Embed-heavy publisher or media site | geolocation, camera, microphone for all third-party embeds by default | autoplay and fullscreen scoped to your known video origins only |
This isn't a guess dressed up as a table. Camera, microphone, geolocation, and payment sit at the top of nearly every published starter policy, OWASP's baseline set among them, because misusing any one of them produces the kind of privacy or billing complaint that lands on a site owner's desk fastest. If you're unsure where to start, start there.
Does Permissions-Policy affect embedded iframes?
Yes, and this is where the header earns its keep. Permissions-Policy set at the top level of your page acts as a ceiling for every iframe on that page, no matter what those iframes ask for individually. An iframe can only use a feature if both the page-level header and the iframe's own allow attribute permit it. Whichever one is stricter wins.
Say your homepage embeds a third-party map widget and an ad unit from a network you don't fully control. Your header disables geolocation everywhere:
Permissions-Policy: geolocation=()
Even if the ad network's iframe includes its own permissive attribute:
<iframe src="https://ads.example.net/unit" allow="geolocation"></iframe>
the geolocation request still fails, because the page-level policy already closed that door before the iframe's own preference was ever evaluated. This is a one-way toggle: the header can narrow what an iframe is allowed to do, but the iframe's allow attribute can never widen a permission the header has already removed. That's exactly the protection most site owners want when they don't fully audit what every third-party script embedded on their site is capable of asking for. The same one-way logic shows up in how frame-ancestors and X-Frame-Options control who can frame your page, so if you're locking down embeds generally, both headers are worth reading together.
Browser support: what to expect across Chrome, Firefox and Safari
Here's the part most reference pages leave out, and it's worth stating plainly rather than glossing over: support for the full Permissions-Policy header is uneven. MDN's own documentation notes that the feature "is not Baseline because it does not work in some of the most widely-used browsers." Chromium-based browsers, Chrome, Edge, Opera, and Brave, support the broadest set of directives through the header itself. Firefox and Safari support fewer directives via the header and lean more heavily on the iframe allow attribute as the primary control instead (MDN, Permissions-Policy header, cited above).
That unevenness is a reason to send the header anyway, not a reason to skip it. A directive a browser doesn't recognize is simply ignored, it won't break the page or throw an error, so there's no downside to sending the full set. What it does mean is that Permissions-Policy shouldn't be treated as your only line of defense against a rogue third-party script requesting the camera. Pair it with the iframe allow attribute on every embed you control, and don't assume every visitor's browser is enforcing the header the same way Chrome does.
A starter Permissions-Policy configuration
OWASP's Secure Headers Project lists Permissions-Policy alongside HSTS, CSP, and Referrer-Policy as part of its recommended baseline header set (OWASP Secure Headers Project). Its commonly cited starting point disables the sensitive APIs almost no site needs by default:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=()
Each line does exactly one thing:
- camera=() blocks getUserMedia() video requests from any origin, including your own.
- microphone=() does the same for audio capture.
- geolocation=() blocks the Geolocation API entirely. If your site has a store locator or a maps widget, change this to
geolocation=(self)instead of dropping the line. - payment=() disables the Payment Request API. Leave this open (
payment=(self)) if you run a checkout flow that uses it. - usb=() and bluetooth=() block the WebUSB and Web Bluetooth APIs, which almost nothing outside of internal admin tools legitimately needs from a public-facing page.
If you delete any one of these lines instead of setting it to (), that feature reverts to the browser's default, unmanaged behavior described earlier in this article, not to "disabled." The header only restricts what it explicitly names.
How to add the header to your server
On Apache, the header directive lives in mod_headers, which most shared hosts already have enabled:
<IfModule mod_headers.c>
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self), payment=(self)"
</IfModule>
The always flag makes sure the header ships on error responses too, not just successful page loads, which matters if you're grading against a tool that might hit a redirect or an error page during its check. For the full picture of mod_headers syntax and where this block belongs in an .htaccess file versus your main server config, the writeup on htaccess.tools covers Apache header directives in more depth than fits here.
On nginx, the equivalent goes inside your server or location block:
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self), payment=(self)" always;
On Cloudflare, you can set it through a Transform Rule without touching origin server config at all, which is the fastest path if your DNS already runs through Cloudflare's proxy. Rather than hand-typing directive names and hoping you spelled a token correctly, this site's header generator builds the full Permissions-Policy line alongside your CSP, HSTS, and the rest of the header set, formatted for Apache, nginx, Caddy, Cloudflare, Netlify, or Vercel.
To verify it worked, reload the page and check the response headers in your browser's network tab, or run the URL back through the security headers checker. If the Permissions-Policy line shows as present with a sensible allowlist instead of a wildcard, the points should register on the next check.
Frequently Asked Questions
What is the Permissions-Policy header?
Permissions-Policy is an HTTP response header that lets a server control which powerful browser features, such as the camera, microphone, geolocation, payment APIs, and fullscreen, a page and its embedded iframes are allowed to invoke. It's set by the server, applies before any script runs, and restricts third-party content on the page to the same allowlist as the rest of the page unless a narrower exception is written for it.
What replaced Feature-Policy?
Feature-Policy was renamed to Permissions-Policy in 2020, and the syntax changed from space-separated quoted tokens (geolocation 'self') to a structured-fields format using parentheses (geolocation=(self)). Browsers ignore the old Feature-Policy header today, so any config copied from a pre-2021 tutorial should be rewritten in the current syntax rather than trusted as-is.
Which features should I disable?
Start with camera, microphone, geolocation, payment, usb, and bluetooth, set to an empty allowlist (()), unless your site actively uses one of them. These are the features most likely to trigger a privacy or billing complaint if a third-party script misuses them, which is why OWASP's baseline Permissions-Policy recommendation anchors on this same shortlist.
Does Permissions-Policy affect embedded iframes?
Yes. The page-level header acts as a ceiling that every iframe on the page inherits, and an iframe's own allow attribute can never grant a permission the header has already removed. Whichever setting is stricter, the header or the attribute, determines what the iframe can actually use, which makes the header an effective way to limit what third-party embeds are capable of requesting even without auditing every script individually.