Skip to content

Instantly share code, notes, and snippets.

@sourcesmith
Last active January 13, 2023 11:18
Show Gist options
  • Save sourcesmith/9d4c75cfdec52d24da9066877a00f4d4 to your computer and use it in GitHub Desktop.
Save sourcesmith/9d4c75cfdec52d24da9066877a00f4d4 to your computer and use it in GitHub Desktop.
nginx config for JS webapp and API endpoints
# Managed by Ansible.
user www-data;
worker_processes {{ worker_processes }};
pid /run/nginx.pid;
worker_rlimit_nofile {{ worker_rlimit_nofile }};
events {
worker_connections {{ worker_connections }};
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
server_tokens off;
#proxy_cache_path /var/lib/nginx/cache levels=1 keys_zone=appmenu:10m;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# Logging Settings
##
access_log /var/log/nginx/access.log combined buffer=16k flush=10s;
error_log /var/log/nginx/error.log;
log_format csp_report '$remote_addr - [$time_local] "$http_referer" "$content_type" "$http_user_agent" "$request_body"';
map $status $csp_loggable {
"200" 1;
default 0;
}
map $content_type $csp_bad_content_type {
default 1;
"application/json" 0;
"application/csp-report" 0;
}
##
# Gzip Settings
##
gzip on;
# Disable for IE <= 6 because there are some known problems
gzip_disable "msie6";
# Add a vary header for downstream proxies to avoid sending cached gzipped files to IE6.
gzip_vary on;
gzip_proxied any;
# https://weblogs.asp.net/owscott/iis-7-compression-good-bad-how-much
gzip_comp_level 4;
gzip_buffers 16 8k;
gzip_http_version 1.1;
# Reduce overhead for small gzip responses.
gzip_min_length 1100;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
### Directive describes the zone, in which the session states are stored i.e. store in slimits. ###
### 1m can handle 32000 sessions with 32 bytes/session, set to 5m x 32000 session ###
limit_conn_zone $binary_remote_addr zone=slimits:1m;
### Control maximum number of simultaneous connections for one session i.e. ###
### restricts the amount of connections from a single ip address ###
limit_conn slimits 1022;
### Rate limiting zone for restricting rate at which auth endpoints can be called. ###
### 10MB should be enough for a history of 160k of addresses.
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
external_domain: subdomain.your-domain.com
app_domain: "app.{{ external_domain }}"
api_domain: "api.{{ external_domain }}"
media_domain: "media.{{ external_domain }}"
api_locations: [
{
context_name: "service_context",
public_hostname: "{{ api_domain }}",
public_port: "443",
# using instance tags.
origin_hostname: "{{ hostvars[groups[\"servicegroup\"][0]][\"private_dns_name\"] }}",
# Using fixed IP.
#origin_hostname: "xxx.xxx.xxx.xxx",
origin_port: "8081",
}
]
content_locations: [
{
name: "content",
path: "/content/",
origin: "https://{{ app_domain }}"
}
]
app_locations: [
{
name: "appname",
path: "/app_path/",
origin: "https://{{ app_domain }}",
sources_domain: "https://*.{{ external_domain }}"
}
]
# of Nginx configuration files in order to fully unleash the power of Nginx.
# http://wiki.nginx.org/Pitfalls
# http://wiki.nginx.org/QuickStart
# http://wiki.nginx.org/Configuration
#
# Generally, you will want to move this file somewhere, and start with a clean
# file but keep this around for reference. Or just disable in sites-enabled.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##
# Default server configuration
#
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
# Disable FLoc
add_header Permissions-Policy interest-cohort=();
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
location /twohundredinator {
access_log off;
allow 127.0.0.1;
return 200;
}
}
map $host $cors_origin {
hostnames;
default "https://{{ app_domain }}";
localhost.{{ external_domain }} 'http://localhost:3000';
}
{% for location in api_locations %}
upstream {{ location["context_name"]|replace('/','_') }}_api {
server {{ location["origin_hostname"] }}:{{ location["origin_port"] }};
keepalive 128;
}
{% endfor %}
server {
# To test TLS connections: openssl s_client -connect {{ api_domain }}:443 -tls1 -tlsextdebug -status
listen 443 ssl http2 reuseport backlog=1024;
#listen [::]:443 ssl http2 ipv6only=on;
# ToDo: Split app domain from api and media domains so locations there can be secured and not bypassed via the
# alternative server names.
server_name {{ api_domain }} {{ media_domain }} {{ app_domain }};
access_log /var/log/nginx/your-site.log combined buffer=16k flush=10s;
error_log /var/log/nginx/your-site_err.log;
proxy_hide_header X-Powered-By;
# config to not allow the browser to render the page inside an frame or iframe
# and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking
# if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri
# https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
add_header X-Frame-Options SAMEORIGIN;
# when serving user-supplied content, include a X-Content-Type-Options: nosniff header along with the Content-Type: header,
# to disable content-type sniffing on some browsers.
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
# currently supported in IE > 8 http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx
# http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
# 'soon' on Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=471020
add_header X-Content-Type-Options nosniff;
# This header enables the Cross-site scripting (XSS) filter built into most recent web browsers.
# It's usually enabled by default anyway, so the role of this header is to re-enable the filter for
# this particular website if it was disabled by the user.
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
add_header X-XSS-Protection "1; mode=block";
ssl_stapling on;
ssl_stapling_verify on;
ssl_certificate /etc/nginx/certs/{{ external_domain }}-fullchain.pem;
ssl_certificate_key /etc/nginx/certs/{{ external_domain }}-key.pem;
# with Content Security Policy (CSP) enabled(and a browser that supports it(http://caniuse.com/#feat=contentsecuritypolicy),
# you can tell the browser that it can only download content from the domains you explicitly allow
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
# https://www.owasp.org/index.php/Content_Security_Policy
# The application code needs to change so we can increase security by disabling 'unsafe-inline' 'unsafe-eval'
# directives for css and js (if you have inline css or js, you will need to keep it too).
# more: http://www.html5rocks.com/en/tutorials/security/content-security-policy/#inline-code-considered-harmful
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'self'; img-src 'self' data:; style-src 'self'";
# enable session resumption to improve https performance
# http://vincent.bernat.im/en/blog/2011-ssl-session-reuse-rfc5077.html
ssl_session_cache shared:SSL:5m; # 1MB can hold around 4000 sessions.
ssl_session_timeout 30m;
ssl_session_tickets off; # Compromises forward secrecy due to poor key rotation. Dead in TLS 1.3 anyway.
# enables server-side protection from BEAST attacks
# http://blog.ivanristic.com/2013/09/is-beast-still-a-threat.html
ssl_prefer_server_ciphers on; # disable SSLv3(enabled by default since nginx 0.8.19) since it's less secure then TLS http://en.wikipedia.org/wiki/Secure_Sockets_Layer#SSL_3.0
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# ciphers chosen for forward secrecy and compatibility
# http://blog.ivanristic.com/2013/08/configuring-apache-nginx-and-openssl-for-forward-secrecy.html
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
# config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
# to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
location / {
root /usr/share/nginx/html/your-site;
index index.html index.htm;
client_max_body_size 20M;
try_files $uri $uri/ =404;
}
location /__cspreport__ {
access_log /var/log/nginx/report-your-site-csp.log csp_report if=$csp_loggable;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1/twohundredinator; # Needs setting up in default.
}
location /nginx_status {
stub_status on;
access_log off;
allow xxx.xxx.xxx.xxx;
deny all;
}
include /etc/nginx/your-site-locations/*;
}
# Managed by Ansible.
location /{{ item["context_name"] }}/ {
client_max_body_size 20M;
# Give site more time to respond.
send_timeout 120;
proxy_send_timeout 120;
#proxy_read_timeout 120;
# https://www.getpagespeed.com/server-setup/nginx/tuning-proxy_buffer_size-in-nginx
proxy_buffers 40 8k;
proxy_busy_buffers_size 20k; # essentially, proxy_buffer_size + 2 small buffers of 8k
# Needed to forward user's IP address.
proxy_set_header X-Real-IP $remote_addr;
# Needed for HTTPS.
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect http://{{ item["origin_hostname"] }}:{{ item["origin_port"] }}/ \"${scheme}://${host}{% if item["public_port"] != "443" %}:{{ item["public_port"] }}{% endif %}/\";
proxy_max_temp_file_size 0;
proxy_pass http://{{ item["context_name"]|replace('/','_') }}_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
set $mutator 0;
if ($request_method = 'POST') {
set $mutator 1;
}
if ($request_method = 'PUT') {
set $mutator 1;
}
if ($request_method = 'PATCH') {
set $mutator 1;
}
if ($request_method = 'DELETE') {
set $mutator 1;
}
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true' always;
#
# Custom headers and headers various browsers *should* be OK with but are not
#
# X-PDQ-CID Only really required for Gatekeeper.
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,If-Modified-Since,If-None-Match,Content-Type,Content-Range,Range,X-PDQ-CID,Vary';
#
# Tell client that this pre-flight info is valid for 7 days.
#
add_header 'Access-Control-Max-Age' 604800;
add_header 'Content-Length' 0;
return 204;
}
if ($mutator) {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'Keep-Alive,User-Agent,Link,Location,If-Modified-Since,ETag,Content-Range,Range';
}
if ($mutator = 0) {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,If-Modified-Since,If-None-Match,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'Keep-Alive,User-Agent,Link,Location,If-Modified-Since,ETag,Content-Range,Range';
}
# {% if item["context_name"] == 'some_basepath' %}
# location /some_basepath/want-to-cache/ {
# proxy_cache cache_for_response; # declared in the http block.
# proxy_cache_key $uri$http_x_pdq_cid;
# proxy_cache_valid 200 170m;
# expires 180m;
# proxy_cache_background_update on;
# proxy_cache_bypass $arg_namespace;
# proxy_no_cache $arg_namespace;
# proxy_pass http://updtream_location_api;
# proxy_ignore_headers Vary;
# }
# {% endif %}
}
# Managed by Ansible.
location {{ item["path"] }} {
client_max_body_size 20M;
try_files $uri {{ item["path"] }}index.html =404;
# Disable FLoC.
add_header Permissions-Policy interest-cohort=();
add_header 'Access-Control-Allow-Origin' '{{ item["origin"] }}';
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS, PUT, PATCH. DELETE';
add_header 'Access-Control-Allow-Credentials' 'true' always;
#
# Custom headers and headers various browsers *should* be OK with but are not
#
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,If-Modified-Since,If-None-Match,Content-Type,Content-Range,Range';
if ($request_method = 'OPTIONS') {
#
# Tell client that this pre-flight info is valid for 7 days.
#
add_header 'Access-Control-Max-Age' 604800;
add_header 'Content-Length' 0;
return 204;
}
add_header Content-Security-Policy "default-src {{ item["sources_domain"] }} 'self'; script-src 'self' 'unsafe-inline' https://*.googleapis.com https://*.firebaseio.com https://app.link; connect-src {{ item["sources_domain"] }} blob: https://*.backblaze.com https://*.googleapis.com https://*.firebaseio.com wss://*.firebaseio.com http://api.giphy.com https://*.branch.io https://{{ media_domain }} https://videodelivery.net; object-src 'self'; font-src 'self' data: https://use.fontawesome.com; img-src * blob: data:; style-src 'self' https://use.fontawesome.com https://fonts.googleapis.com 'unsafe-inline'; media-src https://videodelivery.net blob:; worker-src {{ item["origin"] }} blob:; report-uri /__cspreport__";
}
# Managed by Ansible.
location {{ item["path"] }} {
client_max_body_size 20M;
try_files $uri {{ item["path"] }}index.html =404;
# Disable FLoC.
add_header Permissions-Policy interest-cohort=();
add_header 'Access-Control-Allow-Origin' '{{ item["origin"] }}';
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true' always;
#
# Custom headers and headers various browsers *should* be OK with but are not
#
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,If-Modified-Since,If-None-Match,Content-Type,Content-Range,Range';
if ($request_method = 'OPTIONS') {
#
# Tell client that this pre-flight info is valid for 7 days.
#
add_header 'Access-Control-Max-Age' 604800;
add_header 'Content-Length' 0;
return 204;
}
}
@sourcesmith
Copy link
Author

Files are intended to be under the nginx config dir (e.g. /etc/nginx) in the following structure (which may replace existing files):

nginx.conf
sites-available/default
sites-available/your-site

With a content_context (for static content), a derived app_context for each deployed JS browser app, a derived api_context for each backend server (context) under:
your-site-locations/

It would probably have been better to have had a separate app site config from the API site as the APIs could potentially be called via CDN if someone was being an idiot and the response potentially cached.

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