-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnginx.conf.example
More file actions
188 lines (165 loc) · 11.4 KB
/
nginx.conf.example
File metadata and controls
188 lines (165 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# /etc/nginx/sites-available/cms
# Replace example.com with your domain; Certbot will fill in TLS paths.
# Rate-limit zone for the analytics beacon: 2 requests/second per IP, 10 MB state.
limit_req_zone $binary_remote_addr zone=beacon:10m rate=2r/s;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
root /var/www/cms;
index index.html;
# Match PHP ini upload_max_filesize + post_max_size headroom
client_max_body_size 55M;
# TLS — managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# HSTS — tells browsers to only connect via HTTPS for 2 years.
# Remove includeSubDomains/preload if this domain has HTTP-only subdomains.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# ── Custom error pages ────────────────────────────────────────────────────
error_page 404 /404.html;
# ── Block sensitive directories ───────────────────────────────────────────
location ~* ^/(src|data|content|templates|vendor|bin)(/|$) {
deny all;
return 403;
}
location = /config.php {
deny all;
return 403;
}
# ── Analytics beacon ──────────────────────────────────────────────────────
location = /track.php {
limit_except POST { deny all; }
limit_req zone=beacon burst=20 nodelay;
client_max_body_size 4k;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root/track.php;
fastcgi_param HTTPS on;
add_header X-Content-Type-Options "nosniff" always;
add_header Cache-Control "no-store" always;
}
# ── Media uploads ─────────────────────────────────────────────────────────
# NOTE: Nginx add_header does not inherit from the server block when a
# location defines its own add_header directives. Each location that sets
# any header must repeat all required security headers explicitly.
location /media/ {
alias /var/www/cms/content/media/;
expires 30d;
add_header Cache-Control "public, immutable" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Media files (images, video, audio) need no scripting, fonts, or
# external connections — a strict CSP prevents any SVG or other
# file from executing code if navigated to directly.
add_header Content-Security-Policy "default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'" always;
}
# ── Web fonts ─────────────────────────────────────────────────────────────
# Fonts are content-hashed filenames and never change; cache for 1 year.
location /fonts/ {
expires 1y;
add_header Cache-Control "public, immutable" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'none'; font-src 'self'" always;
}
# ── REST API ──────────────────────────────────────────────────────────────
# All /admin/api/* requests are handled by a single api.php router.
# Nginx longest-prefix match routes this before the /admin/ block below.
location /admin/api {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root/admin/api.php;
fastcgi_param HTTPS on;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# ── Admin panel — PHP-FPM ─────────────────────────────────────────────────
location /admin/ {
index index.php;
try_files $uri $uri/ =404;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS on; # tells PHP $_SERVER['HTTPS'] is set → Secure cookie flag
# Permissive CSP for admin: EasyMDE and PHP templates use inline
# styles and scripts extensively.
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: blob:; media-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
}
}
# ── Pages — stored under pages/ but served at /{slug}/ ───────────────────
# Block direct /pages/... access; content is served via @page below.
location /pages/ {
return 404;
}
# ── Posts — stored under posts/ but served at /YYYY/MM/DD/{slug}/ ────────
# Block direct /posts/... access; content is served via the regex below.
location /posts/ {
return 404;
}
# Date-based post URLs: /YYYY/MM/DD/{slug}/ → posts/YYYY/MM/DD/{slug}/index.html
location ~ "^/[0-9]{4}/[0-9]{2}/[0-9]{2}/" {
rewrite "^/([0-9]{4}/[0-9]{2}/[0-9]{2}/.+\.[^/]+)$" /posts/$1 break;
rewrite ^/(.+?)/?$ /posts/$1/index.html break;
expires 1h;
add_header Cache-Control "public" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://tinylytics.app https://www.googletagmanager.com https://platform.twitter.com https://www.instagram.com; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https:; media-src 'self'; connect-src 'self' https://tinylytics.app https://webmention.io https://www.google-analytics.com; frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://gist.github.com https://www.instagram.com https://platform.twitter.com https://www.linkedin.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
}
# ── Static HTML output ────────────────────────────────────────────────────
location / {
try_files $uri $uri/ $uri/index.html @page;
expires 1h;
add_header Cache-Control "public" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=()" always;
# 'unsafe-inline' required for the inline <script> block in base.php.
# tinylytics.app whitelisted for optional analytics.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://tinylytics.app https://www.googletagmanager.com https://platform.twitter.com https://www.instagram.com; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https:; media-src 'self'; connect-src 'self' https://tinylytics.app https://webmention.io https://www.google-analytics.com; frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://gist.github.com https://www.instagram.com https://platform.twitter.com https://www.linkedin.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
}
# Fallback for pages stored under pages/{slug}/index.html.
# The rewrite strips any trailing slash before constructing the path,
# avoiding double-slash issues when $uri already ends with /.
location @page {
rewrite ^/(.+?)/?$ /pages/$1/index.html break;
expires 1h;
add_header Cache-Control "public" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://tinylytics.app https://www.googletagmanager.com https://platform.twitter.com https://www.instagram.com; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https:; media-src 'self'; connect-src 'self' https://tinylytics.app https://webmention.io https://www.google-analytics.com; frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://gist.github.com https://www.instagram.com https://platform.twitter.com https://www.linkedin.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
}
# ── Feed ──────────────────────────────────────────────────────────────────
location = /feed.xml {
add_header Content-Type "application/atom+xml; charset=utf-8";
add_header X-Content-Type-Options "nosniff" always;
}
# Deny hidden files
location ~ /\. {
deny all;
}
}