How to configure Content Security Policy?

(Rafael Beraldo) #1

Hi all! I hope this question isn’t too noobish, but here I go.

From what I understand, the Discourse Docker image runs its own nginx instance. That’s why I had to setup a reverse proxy in my front-facing nginx server. Furthermore, I fumbled with my config and broke my Discourse instance a little. I was trying to get an A+ on the Mozilla Observatory. That experience taught me the nginx that Discourse is running on has its own configuration and I shouldn’t be setting X-XXS-Protection twice :stuck_out_tongue:

So I went ahead and deleted most of my TLS options on my front-facing nginx. This is how it looks right now:

Output of /etc/nginx/sites-available/

server {
        listen 80;
        listen [::]:80;
        return 301 https://$host$request_uri;

server {
        listen 443 ssl;
        listen [::]:443 ssl;

        ssl on;
        ssl_certificate         /etc/letsencrypt/live/;
        ssl_certificate_key     /etc/letsencrypt/live/;

	# can't use snippets/ssl-params.conf, because the docker nginx sets some of these options
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	ssl_prefer_server_ciphers on;
	ssl_ecdh_curve secp384r1;
	ssl_session_cache shared:SSL:10m;
	ssl_session_tickets off;
	ssl_stapling on;
	ssl_stapling_verify on;
	resolver valid=300s;
	resolver_timeout 5s;
	# Disable preloading HSTS for now.  You can use the commented out header line that includes
	# the "preload" directive if you understand the implications.
	#add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
	add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
	ssl_dhparam /etc/ssl/certs/dhparam.pem;

        location / {
                proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
                proxy_set_header Host $http_host;
                proxy_http_version 1.1;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

	location ~ /.well-known {
		allow all;

However, when I test my domain on the Mozilla Observatory, I get a message that the Content Security Policy header isn’t set.

My question is: how can I configure the Discourse nginx server to implement that header?

(Rafael dos Santos Silva) #2

We don’t support CSP yet. We talked about it sometimes, and it’s something that may be on our future roadmap.

(Rafael Beraldo) #3

Ah, ok. And setting that in my front-facing nginx somehow breaks my Discourse. Is that intended, or just a side effect of the way it works?

(Rafael dos Santos Silva) #4

That’s why we don’t have CSP yet, it breaks Discourse :smile:.

It would take some time to adapt the source code.

Also keep in mind that running Discourse with a outside reverse-proxy implies in more stuff to break, as in your pasted conf, you’re responsible to renewing the SSL certificate, and lost brotli and http2.

(Rafael Beraldo) #5

Fortunately the certificate’s been taken care of, thanks to Let’s Encrypt!. I think I removed http2 after on the listen line when I was troubleshooting why the reverse proxy wasn’t working, when I first setup Discourse. I added it back, but I’m not sure if it is doing any good. It looks like this, now:

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

I’ll read on brotli, I don’t know what it is.

Thanks again for the help!

(Kane York) #6

If you want to find out the answer to that, use the network inspector and see what’s the biggest # of concurrent requests ever satisfied. If it’s more than 6, h2 is helping.

(Rafael Beraldo) #7

Thank you very much @riking! If I read the inspector correctly, http2 is working. Yay!

I now know why CSP isn’t supported by Discourse. I had to go through all my blogs and sites and make lots of exceptions on my nginx config. And I was pulling some images from Wikipedia,, and other sites. Rookie mistake! Funny to look back at the mistakes we made when we were just starting off.

(Mitchell Krog) #8

I hope to see CSP support for Discourse in the near future. Spent a few hours messing with a CSP this morning and yes it completely breaks Discourse no matter what I tried. Will report back though if I find a working solution for this.

UPDATE: as with most wordpress sites plugins and themes, most of them do not support CSP unless you include certain conditions like unsafe-inline, unsafe-eval and data:

Here’s a working CSP I figured out for Discourse which I have running on the site now and it seems to be perfect. Until developers make the actual discourse CSP compliant this is the only way for people actually wanting a CSP on their SSL discourse site.

This is for Nginx by the way, I call external fonts from cloudfront hence they are allowed as img-src and font-src. You will also have to replace the report-uri to your own custom report-uri which you can create and setup at

Here’s my current CSP running on discourse, I have updated this from my original post which now includes allowing using Google Web Fonts, Gravatar User Images and Adsense into the policy. Still working 100% and has not broken Discourse.

add_header Content-Security-Policy "default-src 'self'; script-src 'self' data: 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; media-src 'self' data:; object-src 'self'; child-src 'self'; frame-src 'self' gsa://onpageload https://onpageload ; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests; report-uri";

Most of my wordpress sites have to be configured very similar to the above due to the themes and plugins not being properly CSP compliant.

(Jonathan Claudius) #9

I took a look at this this morning. I believe the easiest way to move forward on a sane CSP policy for Discourse would be simply to enumerate the examples we can fix, turn those into tasks, and then fix them individually. In viewing source on the root of the application, there are 4 examples of inline script in use. If we can move them to external JS references, that moves us a lot closer to dropping the unsafe-inline in favor of something like self or an external hosting location (self is probably better for portability).

I’ve submit a PR for one of those examples here:

If others want to do the same for the other bits, I think we can evolve this to make it more CSP friendly sooner than later.

(Sam Saffron) #10

Replied on the PR but should have the discussion here:

Sorry, this is just not making enough headway

_discourse_javascript.html.erb partial is huge, it would need to be addressed somehow and it is very complex to address because it dynamically generates stuff, also all scripts we add are included via preload_script not raw script tags.

This is just too little imo.

(Jonathan Claudius) #11

@sam thanks for taking a look at the PR and commenting here. I’m actually a little surprised you closed the PR, perhaps I’m not understanding your phrasing. Are you saying that no PRs will be accepted on this front in an incremental state and that it has to be all or nothing PR -or- am I misunderstanding you?

(Sam Saffron) #12

I am saying that this is a very minor change that does not even move an inch towards solving the GIANT CSP problem that is the partial.

I see no point walking a centimeter when we have to run 17 miles.

(Sam Saffron) #14

Sorry, we are not interested in working towards a CSP at the moment, we estimate it will take an enormous amount of effort, we do not have the resources now to work on this.

  • It would involve extensive changes to the theme system
  • Breaking a large number of plugins
  • Extensive changes to the application

This is not a priority for us at the moment, we can revisit in a year.

We will not be accepting any PRs that attempt to move the needle here.

(Sam Saffron) #15