Research

A login page with a CSP you can read the source of.

No unsafe-inline. No unsafe-eval. Nonces on scripts and styles.

login.gov is the single sign-on service for US federal services, and it is built in the open. So you can read its Content Security Policy as code, and every change ever made to it, rather than inferring it from a header.

0
unsafe-* in production
2
Nonced directives
'none'
object-src
The Evidence

The header

This is the full enforced policy on secure.login.gov. Nothing is trimmed, because there is nothing to trim:

secure.login.gov — HTTP response header
content-security-policy: default-src 'self'; child-src 'self';
  form-action 'self'; block-all-mixed-content;
  font-src 'self' data: https://secure.login.gov;
  img-src 'self' data: login.gov …; media-src 'self';
  object-src 'none'; base-uri 'self';
  style-src 'self' https://secure.login.gov 'nonce-640d05ab659d…';
  script-src 'self' https://secure.login.gov https://www.google.com/recaptcha/
    … 'nonce-640d05ab659d…';
  connect-src 'self' www.google-analytics.com

Read it twice and notice what is absent. There is no 'unsafe-inline' and no 'unsafe-eval', anywhere, in any directive. Across this whole body of research that is the only production policy of which that is true — and set against our crawl of the top million sites, where 46.8% of policies carry unsafe-inline, it puts login.gov in a very small minority.

There is a nonce on both script-src and style-src. Most deployments get the script side under control and quietly give up on styles, because inline styles are scattered through templates and frameworks and nobody wants to chase them. Doing both is a decision somebody had to see through.

And then there is form-action 'self', which deserves its own sentence on an identity provider. It constrains where a form on that page is allowed to post to. It is the directive standing between an injected login form quietly shipping someone's credentials to another origin and the browser simply refusing to send them.

Check it yourself:

Verify it yourself
curl -sI https://secure.login.gov/ | grep -i content-security-policy
The Hook

You can read the source

Every other page in this research quotes a header and, where the team wrote one, a blog post. This one can point at the code.

login.gov is developed in the open at 18F/identity-idp, and the policy lives at config/initializers/content_security_policy.rb. The repository is not an archive: it was last pushed to the day before this research was done.

It is worth sitting with what that means. For almost every organisation, a security control is something you infer from the outside — you read the header, you form a view, and the reasoning behind it is private. Here the control is a file. You can read what each directive is built from, see that the asset host comes from configuration rather than being pasted in, and follow the commit history to find out when each decision was taken and what it replaced.

A policy you can read the history of is a policy somebody has been maintaining. A policy you can only read the header of might have been set once, years ago, by someone who has since left.

The Detail

unsafe-eval is one line of code

The most quotable thing in that file is a single line:

config/initializers/content_security_policy.rb
script_src = [:self, IdentityConfig.store.asset_host.presence].compact
script_src << :unsafe_eval if !Rails.env.production?

The dangerous keyword is added for local development and excluded from production by the code itself. Not by a policy document, not by a checklist, not by someone spotting it in review: shipping it is structurally impossible without editing that line and defending the edit.

The same file does the same thing for a webpack development server port and for mailer previews — each added conditionally, none of them ever in production. That is also the answer to the objection that a strict policy makes development miserable. It does, if the production policy is the one you develop against. Here the developer affordances exist, they are written down, and they cannot escape into production.

The Thread

The library that travelled

Twelve commits have touched that policy file. The earliest, from January 2022, is titled “Replace SecureHeaders CSP tooling with built in Rails tooling”.

Which means that before 2022, the login page for US federal services was running its Content Security Policy on secure_headers — the Ruby library that began at Twitter in 2013 and is maintained at GitHub today.

One library, written to solve one company's problem, ending up in another company's hands and then underneath a government identity provider. It is a small thing, but it is the clearest illustration in this research that this is one shared body of practice rather than a series of unrelated deployments.

Being Straight

What this page doesn't claim

The first one matters more than the rest.

This is login.gov, not “the US government”

It is the exception rather than the standard, and pretending otherwise would be the sort of claim this research exists to avoid. The table below is what the rest of the estate actually sends.

script-src is not a pure nonce policy

Alongside 'self' and the nonce it allows reCAPTCHA and Google Analytics hosts. Clean is not the same as minimal.

There is no reporting in the policy

No report-uri and no report-to. When that policy blocks something, nobody is told. We have an obvious interest in pointing that out, so take it as the observation it is rather than a criticism of a team that has clearly thought about this harder than most.

login.gov is not a Report URI customer

Nothing here should be read as US government endorsement of us or of anything else. The page exists because the policy is public, not because anybody is a customer.

Here is the rest of the estate. Every row was read from a live response with a real browser user agent, following redirects:

Federal site Content Security Policy
secure.login.govEnforced, strict, nonce-based
www.cisa.govReport-only, reporting to its own endpoint
www.whitehouse.govupgrade-insecure-requests and frame-ancestors only
www.ssa.govreport-uri and upgrade-insecure-requests only
www.irs.govNone
www.va.govNone
www.healthcare.govNone
cloud.govNone
www.usa.govNone
www.nasa.govNone

Two rows repay a second look. The federal cybersecurity agency runs its policy in report-only with reporting switched on, which is exactly the position GOV.UK spent years in before enforcing. And www.ssa.gov sends an enforced header containing a reporting endpoint and upgrade-insecure-requests — and nothing else. There are no directives in it to violate, so the endpoint has nothing to receive. It is a good illustration of how easily a policy ends up present but not doing anything.

The Point

What this means for you

The transferable lesson is not “be a government”. It is where the policy lives.

login.gov's Content Security Policy is a file in the application, so a change to it is a commit: reviewed, attributed, dated, revertible, and visible to everybody who works on the service. Typed into a CDN console instead, the same change is invisible — no diff, no reviewer, and no way to answer “when did this get weaker, and who did it?” six months later.

Then the second half, which login.gov has deliberately not done. Their policy sends no reports, so when it blocks something, nobody hears about it. That is a defensible choice for a service with one carefully controlled login page and a team that reads its own source. It is a much harder choice for a site with marketing tags, embedded video, a payment provider and four teams shipping to it.

An enforcing policy with no reporting still protects your users. It just never tells you what it stopped, whether the thing it stopped was an attack or your own new checkout script, or that someone weakened it last Tuesday.

That is the half we run. You keep the policy in your own code, exactly as they do, and the reports come back filtered and grouped so you can see what it is actually doing. See how CSP reporting works →

FAQ

Frequently asked questions

default-src 'self', object-src 'none', base-uri 'self', form-action 'self', and a nonce on both script-src and style-src. Neither 'unsafe-inline' nor 'unsafe-eval' appears anywhere in the production policy, which makes it the cleanest one in this research.

It constrains where a form is allowed to post to. On an identity provider that is the directive standing between an injected form quietly shipping someone's credentials to another origin and the browser refusing to send them at all.

It is one line of code: script_src << :unsafe_eval if !Rails.env.production?. The keyword is added for local development and excluded from production by the code itself, so shipping it is not a matter of discipline or of remembering at review time - it is structurally impossible.

No, and the page says so with a table. Of the federal sites checked, login.gov enforces a strict policy, whitehouse.gov sends two directives, and irs.gov, va.gov, healthcare.gov, cloud.gov, usa.gov and nasa.gov send no policy at all. login.gov is the deliberate exception rather than the norm.

No. There is no report-uri or report-to in the policy, so when it blocks something nobody is told. That is the gap: a policy with no reporting still protects users, it just never tells you what it stopped or which of your own scripts it broke.

No, and nothing here should be read as US government endorsement of anything. The page exists because the policy is in public source control, which means you can read it and its history rather than take anyone's word for it.

Put your policy in code. Then watch what it blocks.

Keep the header in your own application, and get the reports back filtered enough to read.