Skip to content

Instantly share code, notes, and snippets.

@deepak
Last active February 22, 2019 22:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save deepak/d0db832088112d200924f1f4d65e733d to your computer and use it in GitHub Desktop.
Save deepak/d0db832088112d200924f1f4d65e733d to your computer and use it in GitHub Desktop.
example nginx config with SSL from LetsEncrypt for a rails app (see steps.txt)
# rails config needs `force_ssl`
# example:
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
# config.force_ssl = true
# and rails needs some headers to be set, otherwise will be trapped in a redirect loop
# nginx config for headers:
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto https;
# proxy_set_header Host $http_host;
# help at
# https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04
upstream example {
server unix:///<path>/shared/tmp/sockets/puma.sock fail_timeout=1;
}
server {
listen 80 default deferred;
server_name example.com;
return 307 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
root <path>/current/public;
access_log <path>/current/log/nginx.access.log;
error_log <path>/current/log/nginx.error.log info;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
# is set by rails to "max-age=31536000"
# so not valid here
# add_header Strict-Transport-Security max-age=15768000;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri @example;
location @example {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://example;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 10M;
keepalive_timeout 10;
}
Followed the digital ocean guide on [Nginx + Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04)
For additional security the guide suggests to generate a `dhparam` file
it is [recommended](http://security.stackexchange.com/questions/38206/can-someone-explain-a-little-better-what-exactly-is-accomplished-by-generation-o) for a new random seed value
command: `sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048`
After the `Let's Encrypt` setup is done, we will use the following certificate files:
- fullchain.pem
- privkey.pem
They are stored under the `/etc/letsencrypt/live/some.example.com/` folder
replace `some.example.com` with your website
Now that the certificates are already generated, the steps to steps to configure `Nginx` + `Puma (the rails server` are:
- edit the site config. will be under `/etc/nginx/sites-enabled/site-name` to add `ssl`
am guessing ssl is not configuered.
there might be a `listen 80` or something like it
or there will be no `listen` directive at all, which as per the [docs](http://nginx.org/en/docs/http/ngx_http_core_module.html#listen) means that:
```
If the directive is not present then either *:80 is used
if nginx runs with the superuser privileges, or *:8000 otherwise.
```
`ssl` is not on by default so we need to add it
change it to something like:
```
server {
listen 443 ssl;
server_name some.example.com;
ssl_certificate /etc/letsencrypt/live/some.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/some.example.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
# is set by rails to "max-age=31536000"
# so not valid here
# add_header Strict-Transport-Security max-age=15768000;
...
}
```
The Digital Ocean [guide](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04) asks us to add a `add_header Strict-Transport-Security max-age=15768000;` header. but [Rails](http://rubyonrails.org) already sets it for us. Adding it here duplicates the same header in the response, but with different values.
so the response looks like:
```
add_header Strict-Transport-Security max-age=15768000;
add_header Strict-Transport-Security max-age=31536000;
```
This is not what we want. No error is shown on the browser. But am not sure which value is used and [ssllabs](www.ssllabs.com/ssltest/analyze.html) throws a warning. So let [Rails](http://rubyonrails.org) manage it for us.
- make sure `http` redirects to `https`
```
server {
listen 80 deferred;
server_name some.example.com;
return 307 https://example.com$request_uri;
}
```
Again some changes here from the Digital Ocean [guide](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
- they use [301 Moved Permanently](http://httpstatus.es/301). But we use a [307 Temporary Redirect](https://httpstatuses.com/307). Just in case we change our mind. Although I cannot think of a good reason why we would want to do that :-)
- `deferred` is just a linux only performance micro-tweak. see [h5bp](https://github.com/h5bp/server-configs-nginx/issues/100)
- reload nginx with a `sudo service nginx reload`
- test that the website works by going to the `https` url eg. `https://some.example.com`
and that the `http` url redirects to the https version
- lint that the `ssl` setup is correct by visiting `ssllabs`. eg. url is `https://www.ssllabs.com/ssltest/analyze.html?d=some.example.com`. replace the param at the end with your own website url
A potential cause for error might be that the ruby http/s parser cannot recognize the https request. Rails and most of the http servers depend on the `host` header being set for https
```
location @example {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://example;
}
```
`proxy_set_header Host $http_host;` is https specific. Others are for the reverse-proxy.
Also notice that, in the above code-snippet, we proxy to `http` not `https`. Otherwise we will get error like
```
if proxying to https, logs will look like
==> /shared/log/nginx.error.log <==
2016/04/12 09:05:34 [info] 8373#0: *347 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:35 [info] 8373#0: *348 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:35 [info] 8373#0: *349 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:35 [info] 8373#0: *350 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:36 [info] 8373#0: *351 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:36 [info] 8373#0: *352 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:37 [info] 8373#0: *353 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:37 [info] 8373#0: *354 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:19:03 [info] 8373#0: *359 recv() failed (104: Connection reset by peer) while SSL handshaking, client: 5.45.64.228, server: 0.0.0.0:443
2016/04/12 09:23:20 [error] 8373#0: *361 SSL_do_handshake() failed (SSL: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol) while SSL handshaking to upstream, client: 202.83.38.120, server: example.com, request: "GET / HTTP/1.1", upstream: "https://unix:///shared/tmp/sockets/puma.sock:/", host: "example.com"
==> /shared/log/puma.access.log <==
---
2016-04-12 09:03:30 +0200: HTTP parse error, malformed request (127.0.0.1): #<Puma::HttpParserError: Invalid HTTP format, parsing fails.>
2016-04-12 09:03:30 +0200: ENV: {"rack.version"=>[1, 3], "rack.errors"=>#<File:/current/log/puma.access.log>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "SCRIPT_NAME"=>"", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"2.16.0", "GATEWAY_INTERFACE"=>"CGI/1.2", "REMOTE_ADDR"=>"127.0.0.1"}
```
in the rails server logs. [puma](http://puma.io) in this case
@deepak
Copy link
Author

deepak commented Apr 12, 2016

if proxying to https, logs will look like

==> /shared/log/nginx.error.log <==
2016/04/12 09:05:34 [info] 8373#0: *347 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:35 [info] 8373#0: *348 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:35 [info] 8373#0: *349 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:35 [info] 8373#0: *350 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:36 [info] 8373#0: *351 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:36 [info] 8373#0: *352 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:37 [info] 8373#0: *353 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:05:37 [info] 8373#0: *354 peer closed connection in SSL handshake while SSL handshaking, client: <some-ip>, server: 0.0.0.0:443
2016/04/12 09:19:03 [info] 8373#0: *359 recv() failed (104: Connection reset by peer) while SSL handshaking, client: 5.45.64.228, server: 0.0.0.0:443
2016/04/12 09:23:20 [error] 8373#0: *361 SSL_do_handshake() failed (SSL: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol) while SSL handshaking to upstream, client: 202.83.38.120, server: example.com, request: "GET / HTTP/1.1", upstream: "https://unix:///shared/tmp/sockets/puma.sock:/", host: "example.com"

==> /shared/log/puma.access.log <==

---
2016-04-12 09:03:30 +0200: HTTP parse error, malformed request (127.0.0.1): #<Puma::HttpParserError: Invalid HTTP format, parsing fails.>
2016-04-12 09:03:30 +0200: ENV: {"rack.version"=>[1, 3], "rack.errors"=>#<File:/current/log/puma.access.log>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "SCRIPT_NAME"=>"", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"2.16.0", "GATEWAY_INTERFACE"=>"CGI/1.2", "REMOTE_ADDR"=>"127.0.0.1"}

@opya
Copy link

opya commented Jan 30, 2018

works for me 👍 , thanks

@reicheltd
Copy link

"and rails needs some headers to be set, otherwise will be trapped in a redirect loop". Exactly what fixed my problem with my config. I was in that https loop. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment