commit 7fcc22f3c3c19e9891dffa21ddf78785e01d84f7 Author: Michał 'rysiek' Woźniak Date: Thu Apr 4 13:11:54 2019 +0200 initial import diff --git a/README.md b/README.md new file mode 100644 index 0000000..c597773 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Fasada - a front-end cache and reverse proxy config + +A front-end cache and reverse proxy config, based on `nginx`, with Tor thrown in for good measure. + +## Idea + +The basic idea is to have a minimal front-end-cache config that can be spun-up (or indeed, that's just running) on a public server and is able to cache and serve a WordPress website effectively. + +This includes strict caching of all content, even dynamic one, in a way that takes the load off of the PHP backend, and that is able to serve cached content for a long period of time in case of the backend not being available (due to maintenance or technical problems). + +## Operation + +Static resources (CSS, JS, images, etc) should be cached for long time (say, 24 hours or more); cookies on such static resources should be ignored. + +Public dynamic resources should be cached for a short time (say, 1 minute), cookies should be ignored/removed. + +Private dynamic content (admin pages, etc) should not be cached, at all. Ideally, they would be served from a dedicated domain (`admin.example.com`), which would not be cached, but would also be accessible only via a VPN or some such. + +## Configuration + +[Upstreams](http://nginx.org/en/docs/http/ngx_http_upstream_module.html) configuration should put in [`services/etc/nginx/conf.d/upstreams.conf`](services/etc/nginx/conf.d/upstreams.conf) file, so that tests can make use of them. + +## Testing + +Automated tests are provided, using [BATS](https://github.com/bats-core/bats-core/) (which is included as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules)). When deploying Fasada remember to initialized and pull the submodule: + +``` +git submodule init +git submodule update +``` + +Once that's done, tests are available via the `./tests/runtests.sh` command. Upon running it will: + +1. use `./tests/gentests.sh` to generate upstream-specific tests +1. run them on the host server +1. run them via `docker-compose exec` in the `nginx` container + +## Things to consider + +**Q: How should we handle apparent `IP:port` clash between the upstream config and `fasada`? There are going to have to be two `nginx` services running on public ports, right?** +**A**: Two ways to go around it: + - run them on different IPs; + - run them on different ports. + +The `fasada` will have to run on public `IP` and ports `80` and `443`, no way around it. We're running `nginx` in a `docker` container, and while there is a way to tell `docker-compose` that a certain port should only be exposed on a certain IP, that would require specific configuration for specific hosts (that is, putting specific `IP` addresses in the `docker-compose.yml` file) - something we want to avoid. +Hence the sane solution is to run `nginx` from `fasada` on ports `80` and `443`, and the `server-config` one on other ports (say, `10080` and `10443`). + +**Q: Where should we handle setting cache control headers?** +**A**: Apparently the right answer is: ["upstream"](https://serversforhackers.com/nginx-caching/). + +# Wait, why nginx? + +There are software solutions that are hand-crafted to be reverse proxies and front-end-cache solutions, why are we using `nginx`? Well... + +Mainly: no time to play with other solutions and learn them, `nginx` does the job well enough. + +But we did look at `varnish`, and we found [it does not support SSL and has no intention to](https://www.varnish-cache.org/docs/trunk/phk/ssl_again.html). We would have to run `nginx` *in front* of `varnish` that would then be *in front of* our upstream `nginx` servers. This is madness. + +# ToDo + +This needs to be documented better, both using comments in code, and using this README file, and the one in the `services/etc/nginx` subdirectory. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ecdc83d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +nginx: + build: https://github.com/occrp/watchful-nginx.git + ports: + - "80:80" + - "443:443" + volumes: + - "./services/etc/nginx/:/etc/nginx/:ro" # config + - "./tests/:/opt/tests/:ro" # tests + - "/srv/data/secrets/nginx/:/etc/ssl/nginx/" # this is where dhparam goes; maybe we should use a data container for this + - "/srv/data/cache/fasada/:/srv/data/cache/nginx" # cache; maybe we should use a data container for this? + - "/srv/logs/fasada/:/srv/logs/nginx/" # logs + # letsencrypt + - "/srv/data/secrets/letsencrypt/archive/:/srv/data/secrets/letsencrypt/archive/:ro" # LetsEncrypt certificate store, containing all the certs ever issued + - "/srv/data/secrets/letsencrypt/live/:/srv/data/secrets/letsencrypt/live/:ro" # LetsEncrypt live certificate store, containing symlinks to the most current certificates for a given domain + +tor: + image: vpetersson/torrelay + user: debian-tor + volumes: + - "./services/etc/tor/:/etc/tor/:ro" + - "/srv/data/secrets/tor/:/var/lib/tor/web/" # apparently tor has to have RW acess to this directory; + # TODO make private_key read-only? + links: + - nginx + command: ["/usr/sbin/tor", "-f", "/etc/tor/torrc"] diff --git a/services/etc/nginx/README.md b/services/etc/nginx/README.md new file mode 100644 index 0000000..098d805 --- /dev/null +++ b/services/etc/nginx/README.md @@ -0,0 +1,3 @@ +This is where we're working on building new Nginx config files for everything on this server. + +ToDo: Document it better. diff --git a/services/etc/nginx/koi-utf b/services/etc/nginx/koi-utf new file mode 100644 index 0000000..e7974ff --- /dev/null +++ b/services/etc/nginx/koi-utf @@ -0,0 +1,109 @@ + +# This map is not a full koi8-r <> utf8 map: it does not contain +# box-drawing and some other characters. Besides this map contains +# several koi8-u and Byelorussian letters which are not in koi8-r. +# If you need a full and standard map, use contrib/unicode2nginx/koi-utf +# map instead. + +charset_map koi8-r utf-8 { + + 80 E282AC ; # euro + + 95 E280A2 ; # bullet + + 9A C2A0 ; #   + + 9E C2B7 ; # · + + A3 D191 ; # small yo + A4 D194 ; # small Ukrainian ye + + A6 D196 ; # small Ukrainian i + A7 D197 ; # small Ukrainian yi + + AD D291 ; # small Ukrainian soft g + AE D19E ; # small Byelorussian short u + + B0 C2B0 ; # ° + + B3 D081 ; # capital YO + B4 D084 ; # capital Ukrainian YE + + B6 D086 ; # capital Ukrainian I + B7 D087 ; # capital Ukrainian YI + + B9 E28496 ; # numero sign + + BD D290 ; # capital Ukrainian soft G + BE D18E ; # capital Byelorussian short U + + BF C2A9 ; # (C) + + C0 D18E ; # small yu + C1 D0B0 ; # small a + C2 D0B1 ; # small b + C3 D186 ; # small ts + C4 D0B4 ; # small d + C5 D0B5 ; # small ye + C6 D184 ; # small f + C7 D0B3 ; # small g + C8 D185 ; # small kh + C9 D0B8 ; # small i + CA D0B9 ; # small j + CB D0BA ; # small k + CC D0BB ; # small l + CD D0BC ; # small m + CE D0BD ; # small n + CF D0BE ; # small o + + D0 D0BF ; # small p + D1 D18F ; # small ya + D2 D180 ; # small r + D3 D181 ; # small s + D4 D182 ; # small t + D5 D183 ; # small u + D6 D0B6 ; # small zh + D7 D0B2 ; # small v + D8 D18C ; # small soft sign + D9 D18B ; # small y + DA D0B7 ; # small z + DB D188 ; # small sh + DC D18D ; # small e + DD D189 ; # small shch + DE D187 ; # small ch + DF D18A ; # small hard sign + + E0 D0AE ; # capital YU + E1 D090 ; # capital A + E2 D091 ; # capital B + E3 D0A6 ; # capital TS + E4 D094 ; # capital D + E5 D095 ; # capital YE + E6 D0A4 ; # capital F + E7 D093 ; # capital G + E8 D0A5 ; # capital KH + E9 D098 ; # capital I + EA D099 ; # capital J + EB D09A ; # capital K + EC D09B ; # capital L + ED D09C ; # capital M + EE D09D ; # capital N + EF D09E ; # capital O + + F0 D09F ; # capital P + F1 D0AF ; # capital YA + F2 D0A0 ; # capital R + F3 D0A1 ; # capital S + F4 D0A2 ; # capital T + F5 D0A3 ; # capital U + F6 D096 ; # capital ZH + F7 D092 ; # capital V + F8 D0AC ; # capital soft sign + F9 D0AB ; # capital Y + FA D097 ; # capital Z + FB D0A8 ; # capital SH + FC D0AD ; # capital E + FD D0A9 ; # capital SHCH + FE D0A7 ; # capital CH + FF D0AA ; # capital hard sign +} diff --git a/services/etc/nginx/koi-win b/services/etc/nginx/koi-win new file mode 100644 index 0000000..72afabe --- /dev/null +++ b/services/etc/nginx/koi-win @@ -0,0 +1,103 @@ + +charset_map koi8-r windows-1251 { + + 80 88 ; # euro + + 95 95 ; # bullet + + 9A A0 ; #   + + 9E B7 ; # · + + A3 B8 ; # small yo + A4 BA ; # small Ukrainian ye + + A6 B3 ; # small Ukrainian i + A7 BF ; # small Ukrainian yi + + AD B4 ; # small Ukrainian soft g + AE A2 ; # small Byelorussian short u + + B0 B0 ; # ° + + B3 A8 ; # capital YO + B4 AA ; # capital Ukrainian YE + + B6 B2 ; # capital Ukrainian I + B7 AF ; # capital Ukrainian YI + + B9 B9 ; # numero sign + + BD A5 ; # capital Ukrainian soft G + BE A1 ; # capital Byelorussian short U + + BF A9 ; # (C) + + C0 FE ; # small yu + C1 E0 ; # small a + C2 E1 ; # small b + C3 F6 ; # small ts + C4 E4 ; # small d + C5 E5 ; # small ye + C6 F4 ; # small f + C7 E3 ; # small g + C8 F5 ; # small kh + C9 E8 ; # small i + CA E9 ; # small j + CB EA ; # small k + CC EB ; # small l + CD EC ; # small m + CE ED ; # small n + CF EE ; # small o + + D0 EF ; # small p + D1 FF ; # small ya + D2 F0 ; # small r + D3 F1 ; # small s + D4 F2 ; # small t + D5 F3 ; # small u + D6 E6 ; # small zh + D7 E2 ; # small v + D8 FC ; # small soft sign + D9 FB ; # small y + DA E7 ; # small z + DB F8 ; # small sh + DC FD ; # small e + DD F9 ; # small shch + DE F7 ; # small ch + DF FA ; # small hard sign + + E0 DE ; # capital YU + E1 C0 ; # capital A + E2 C1 ; # capital B + E3 D6 ; # capital TS + E4 C4 ; # capital D + E5 C5 ; # capital YE + E6 D4 ; # capital F + E7 C3 ; # capital G + E8 D5 ; # capital KH + E9 C8 ; # capital I + EA C9 ; # capital J + EB CA ; # capital K + EC CB ; # capital L + ED CC ; # capital M + EE CD ; # capital N + EF CE ; # capital O + + F0 CF ; # capital P + F1 DF ; # capital YA + F2 D0 ; # capital R + F3 D1 ; # capital S + F4 D2 ; # capital T + F5 D3 ; # capital U + F6 C6 ; # capital ZH + F7 C2 ; # capital V + F8 DC ; # capital soft sign + F9 DB ; # capital Y + FA C7 ; # capital Z + FB D8 ; # capital SH + FC DD ; # capital E + FD D9 ; # capital SHCH + FE D7 ; # capital CH + FF DA ; # capital hard sign +} diff --git a/services/etc/nginx/mime.types b/services/etc/nginx/mime.types new file mode 100644 index 0000000..89be9a4 --- /dev/null +++ b/services/etc/nginx/mime.types @@ -0,0 +1,89 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + + application/font-woff woff; + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; + application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/services/etc/nginx/nginx.conf b/services/etc/nginx/nginx.conf new file mode 100644 index 0000000..9045b6c --- /dev/null +++ b/services/etc/nginx/nginx.conf @@ -0,0 +1,190 @@ + +user www-data www-data; + +worker_processes 8; + +pid /var/run/nginx.pid; + +events { + worker_connections 768; + multi_accept on; + use epoll; +} + +http { + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + types_hash_max_size 2048; + server_tokens off; + + server_names_hash_max_size 2048; + server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include mime.types; + default_type application/octet-stream; + + # default charset + charset utf-8; + + ## + # Some of SSL settings are in snippets/ssl.conf + # included in all ssl-enabled hosts + ## + + # Mozilla SSL Intermediate profile + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + ssl_prefer_server_ciphers on; + + # cache SSL sessions for 10m (this is about 40,000 sessions), timing them out + # after 24 hours. + # https://sethvargo.com/getting-an-a-plus-on-qualys-ssl-labs-tester/ + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 24h; + + # set the buffer size to 1400 bytes (that way it fits into a single MTU). + ssl_buffer_size 1400; + + # this is generated in docker/nginx/run.sh + ssl_dhparam '/etc/ssl/nginx/dhparam.pem'; + + ## + # Default Logging Settings + ## + + access_log /srv/logs/nginx/access.log combined; + error_log /srv/logs/nginx/error.log; + + ###################################################################### + ## Various configuration sections: ## + ###################################################################### + + ## + # Gzip Settings + ## + + gzip on; + gzip_disable "msie6"; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml; + + + ## + # Size Limits & Buffer Overflows + ## + + #client_header_buffer_size 1k; + client_max_body_size 20m; + client_body_buffer_size 1m; + #large_client_header_buffers 2 1k; + + ## + # Start: Timeouts + ## + client_body_timeout 10; + client_header_timeout 10; + keepalive_timeout 5 5; + send_timeout 10; + + # 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:5m; + + # Control maximum number of simultaneous connections for one session i.e. + # restricts the amount of connections from a single ip address + # limit_conn slimits 5; + + + # + # rate limiting + # https://www.nginx.com/blog/rate-limiting-nginx/ + # https://nginx.org/en/docs/http/ngx_http_limit_req_module.html + # + # just add "limit_req zone=ddosed burst=100;" in any location + # that is supposed to be rate-limited + limit_req_zone $binary_remote_addr zone=ddosed:20m rate=10r/s; + + ## + # Proxy cache settings + ## + + # keys_zone=fasada:10m - how long the cache is considered fresh + # max_size=3G - we're fine with large cache size on disk + # inactive=12h - stale cached content is retained in cache for 12h at least + # this is necessary if we want to be able to serve stale content + # in case of errors longer than the time the cache is considered "fresh" + # which we do -- this gives us the ability to survive a backend crash + # with most users not noticing + proxy_cache_path /srv/data/cache/nginx/proxy/ levels=1:2 keys_zone=fasada:10m max_size=3G inactive=12h; + + # + # A cached response is first written to a temporary file, and then the file is renamed. + # Starting from version 0.8.9, temporary files and the cache can be put on different file systems. + # However, be aware that in this case a file is copied across two file systems instead of + # the cheap renaming operation. It is thus recommended that for any given location both + # cache and a directory holding temporary files are put on the same file system. The directory + # for temporary files is set based on the use_temp_path parameter (1.7.10). + # + proxy_temp_path /srv/data/cache/nginx/tmp/ 1 2; + + # no fastcgi around + #fastcgi_cache_path /srv/data/cache/nginx/fastcgi/ levels=1:2 keys_zone=fastcgicache:100m max_size=5G; + + proxy_cache_key $scheme:$request_method:$host/$uri$is_args$args; + # no fastcgi around + #fastcgi_cache_key "$proxy_host|$request_method|$scheme|$host|$request_uri|$is_args|$args|$cookie_user|$cookie_phpsessid"; + + # by default, bypass the cache when: + # - Pragma: no-cache is present + # - any cookie is present + proxy_cache_bypass $http_pragma $http_cookie; + + proxy_set_header Host $http_host; + 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 $scheme; + + proxy_intercept_errors on; + proxy_buffering on; + proxy_buffer_size 128k; + proxy_buffers 256 16k; + proxy_busy_buffers_size 256k; + proxy_temp_file_write_size 256k; + proxy_max_temp_file_size 0; + + # Defines a timeout for establishing a connection with a proxied server. + # It should be noted that this timeout cannot usually exceed 75 seconds. + proxy_connect_timeout 2; + + # Sets a timeout for transmitting a request to the proxied server. + # The timeout is set only between two successive write operations, + # not for the transmission of the whole request. + # If the proxied server does not receive anything within this time, + # the connection is closed. + proxy_send_timeout 120; + + # Defines a timeout for reading a response from the proxied server. + # The timeout is set only between two successive read operations, + # not for the transmission of the whole response. + # If the proxied server does not transmit anything within this time, + # the connection is closed. + proxy_read_timeout 5; + + + ## + # Virtual Host Configs + ## + + include sites/*.conf; +} diff --git a/services/etc/nginx/sites/000-catchall.conf b/services/etc/nginx/sites/000-catchall.conf new file mode 100644 index 0000000..b38e539 --- /dev/null +++ b/services/etc/nginx/sites/000-catchall.conf @@ -0,0 +1,28 @@ +# require TLS, redirect to https://www.example.com/ +server { + listen 80 default_server; + listen 443 ssl default_server; + server_name _; + + # tls letsencrypt stateless acme config + # no need for webroot and stuff + # + # this is described for acme.sh, + # but should work with any LE client + # https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode + location ~ "^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$" { + default_type text/plain; + return 200 "$1."; + } + + + # ssl keycert + ssl_certificate /srv/data/ssl/sites/www.example.com.cert; + ssl_certificate_key /srv/data/ssl/sites/www.example.com.key; + + location / { + return 301 https://www.example.com/; + } + access_log /srv/logs/nginx/catchall.access.log combined; + error_log /srv/logs/nginx/catchall.error.log error; +} diff --git a/services/etc/nginx/sites/admin.example.com.conf b/services/etc/nginx/sites/admin.example.com.conf new file mode 100644 index 0000000..0a40bb7 --- /dev/null +++ b/services/etc/nginx/sites/admin.example.com.conf @@ -0,0 +1,51 @@ +# www.example.com website +server { + listen 80; + listen 443 ssl; + server_name admin.example.com; + + # general vhost settings + access_log /srv/logs/nginx/admin.example.com.access.log combined; + error_log /srv/logs/nginx/admin.example.com.error.log error; + + # ssl keycert + ssl_certificate /srv/data/secrets/letsencrypt/live/admin.example.com/fullchain.pem; + ssl_certificate_key /srv/data/secrets/letsencrypt/live/admin.example.com/privkey.pem; + + # TLS settings + # can't set headers in an if that is *not* in a location, + # so we need to work around this + add_header Strict-Transport-Security "max-age=31536000"; + + # proxy params, mainly for properly tracking visitors + proxy_set_header Host $http_host; + 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 $scheme; + # needed for keepalive to work + proxy_set_header Connection ""; + proxy_http_version 1.1; + + # tls letsencrypt stateless acme config + # no need for webroot and stuff + # + # this is described for acme.sh, + # but should work with any LE client + # https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode + location ~ "^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$" { + default_type text/plain; + return 200 "$1."; + } + + # set proxy zone + proxy_cache off; + + # reverse proxy to upstream + location / { + # debugging + add_header X-Proxy-Cache $upstream_cache_status; + #include snippets/security.conf; + proxy_pass http://127.0.0.1:10080; + } + +} diff --git a/services/etc/nginx/sites/example.com.conf b/services/etc/nginx/sites/example.com.conf new file mode 100644 index 0000000..0d31e72 --- /dev/null +++ b/services/etc/nginx/sites/example.com.conf @@ -0,0 +1,129 @@ +# www.example.com website +server { + listen 80; + listen 443 ssl; + server_name www.example.com example.com; + + # general vhost settings + access_log /srv/logs/nginx/example.com.access.log combined; + error_log /srv/logs/nginx/example.com.error.log error; + + # ssl keycert + ssl_certificate /srv/data/secrets/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /srv/data/secrets/letsencrypt/live/example.com/privkey.pem; + + # TLS settings + # can't set headers in an if that is *not* in a location, + # so we need to work around this + add_header Strict-Transport-Security "max-age=31536000"; + + # TLS letsencrypt stateless acme config + # no need for webroot and stuff + # + # this is described for acme.sh, + # but should work with any LE client + # https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode + location ~ "^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$" { + default_type text/plain; + return 200 "$1."; + } + + # proxy params, mainly for properly tracking visitors + proxy_set_header Host $http_host; + 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 $scheme; + # needed for keepalive to work + proxy_set_header Connection ""; + proxy_http_version 1.1; + + # proxy zone + proxy_cache fasada; + # use stale cached resources in case upstream is not available for some reason + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + proxy_cache_background_update on; + proxy_cache_revalidate on; + proxy_cache_valid 200 1h; + proxy_cache_lock on; + + + # admin area *have to* be uncached; blocking here + # should be made available on admin.domain.tld + location ~* ^/(wp-admin|admin|login|wp-login|signin).* { + add_header X-Proxy-Cache $upstream_cache_status; + return 403; + } + + # WordPress themes + location ~* ^/wp-content/themes/.* { + + # forced cache + proxy_cache_bypass 0; + proxy_hide_header Set-Cookie; + proxy_hide_header Expires; + proxy_hide_header Cache-Control; + proxy_hide_header Pragma; + proxy_ignore_headers Set-Cookie Expires Cache-Control; + add_header Cache-Control "public"; + expires 30m; + add_header X-Proxy-Cache-WP themes; + + # debugging + add_header X-Proxy-Cache $upstream_cache_status; + + # no need for access log for these + access_log off; + proxy_pass http://127.0.0.1:10080; + + } + + # robots.txt, favicons, apple icons, etc + location ~* .*/(robots\.txt|favicon\.ico|apple-touch-icon\.png|apple-touch-icon-precomposed\.png)$ { + + # forced cache + proxy_cache_bypass 0; + add_header Cache-Control "public"; + proxy_cache_valid 200 301 302 303 307 308 5h; + expires 5h; + + # debugging + add_header X-Proxy-Cache $upstream_cache_status; + + # no need for access log for these + access_log off; + proxy_pass http://127.0.0.1:10080; + } + + # images and other static resources + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|json|woff|woff2|ttf|otf|bmp|cur|gz|svgz|mp4|ogg|ogv|webm|htc|mp4|mpeg|mp3|txt|pdf)$ { + + # forced cache + proxy_cache_bypass 0; + add_header Cache-Control "public"; + proxy_cache_valid 200 301 302 303 307 308 1h; + expires 1h; + + add_header X-Proxy-Cache $upstream_cache_status; + + proxy_pass http://127.0.0.1:10080; + } + + # reverse proxy to upstream, for *everything else* + # caching for 1 minute + location / { + # forced cache + proxy_cache_bypass 0; + proxy_hide_header Set-Cookie; + proxy_hide_header Expires; + proxy_hide_header Cache-Control; + proxy_hide_header Pragma; + proxy_ignore_headers Set-Cookie Expires Cache-Control X-Accel-Expires; + add_header Cache-Control "no-store"; + proxy_cache_valid 200 301 302 303 307 308 20s; + add_header X-Proxy-Cache $upstream_cache_status; + add_header Content-Security-Policy "frame-ancestors 'self'"; + add_header X-Frame-Options SAMEORIGIN; + proxy_pass http://127.0.0.1:10080; + } + +} diff --git a/services/etc/nginx/win-utf b/services/etc/nginx/win-utf new file mode 100644 index 0000000..f376943 --- /dev/null +++ b/services/etc/nginx/win-utf @@ -0,0 +1,125 @@ +# This map is not a full windows-1251 <> utf8 map: it does not +# contain Serbian and Macedonian letters. If you need a full map, +# use contrib/unicode2nginx/win-utf map instead. + +charset_map windows-1251 utf-8 { + + 82 E2809A; # single low-9 quotation mark + + 84 E2809E; # double low-9 quotation mark + 85 E280A6; # ellipsis + 86 E280A0; # dagger + 87 E280A1; # double dagger + 88 E282AC; # euro + 89 E280B0; # per mille + + 91 E28098; # left single quotation mark + 92 E28099; # right single quotation mark + 93 E2809C; # left double quotation mark + 94 E2809D; # right double quotation mark + 95 E280A2; # bullet + 96 E28093; # en dash + 97 E28094; # em dash + + 99 E284A2; # trade mark sign + + A0 C2A0; #   + A1 D18E; # capital Byelorussian short U + A2 D19E; # small Byelorussian short u + + A4 C2A4; # currency sign + A5 D290; # capital Ukrainian soft G + A6 C2A6; # borken bar + A7 C2A7; # section sign + A8 D081; # capital YO + A9 C2A9; # (C) + AA D084; # capital Ukrainian YE + AB C2AB; # left-pointing double angle quotation mark + AC C2AC; # not sign + AD C2AD; # soft hypen + AE C2AE; # (R) + AF D087; # capital Ukrainian YI + + B0 C2B0; # ° + B1 C2B1; # plus-minus sign + B2 D086; # capital Ukrainian I + B3 D196; # small Ukrainian i + B4 D291; # small Ukrainian soft g + B5 C2B5; # micro sign + B6 C2B6; # pilcrow sign + B7 C2B7; # · + B8 D191; # small yo + B9 E28496; # numero sign + BA D194; # small Ukrainian ye + BB C2BB; # right-pointing double angle quotation mark + + BF D197; # small Ukrainian yi + + C0 D090; # capital A + C1 D091; # capital B + C2 D092; # capital V + C3 D093; # capital G + C4 D094; # capital D + C5 D095; # capital YE + C6 D096; # capital ZH + C7 D097; # capital Z + C8 D098; # capital I + C9 D099; # capital J + CA D09A; # capital K + CB D09B; # capital L + CC D09C; # capital M + CD D09D; # capital N + CE D09E; # capital O + CF D09F; # capital P + + D0 D0A0; # capital R + D1 D0A1; # capital S + D2 D0A2; # capital T + D3 D0A3; # capital U + D4 D0A4; # capital F + D5 D0A5; # capital KH + D6 D0A6; # capital TS + D7 D0A7; # capital CH + D8 D0A8; # capital SH + D9 D0A9; # capital SHCH + DA D0AA; # capital hard sign + DB D0AB; # capital Y + DC D0AC; # capital soft sign + DD D0AD; # capital E + DE D0AE; # capital YU + DF D0AF; # capital YA + + E0 D0B0; # small a + E1 D0B1; # small b + E2 D0B2; # small v + E3 D0B3; # small g + E4 D0B4; # small d + E5 D0B5; # small ye + E6 D0B6; # small zh + E7 D0B7; # small z + E8 D0B8; # small i + E9 D0B9; # small j + EA D0BA; # small k + EB D0BB; # small l + EC D0BC; # small m + ED D0BD; # small n + EE D0BE; # small o + EF D0BF; # small p + + F0 D180; # small r + F1 D181; # small s + F2 D182; # small t + F3 D183; # small u + F4 D184; # small f + F5 D185; # small kh + F6 D186; # small ts + F7 D187; # small ch + F8 D188; # small sh + F9 D189; # small shch + FA D18A; # small hard sign + FB D18B; # small y + FC D18C; # small soft sign + FD D18D; # small e + FE D18E; # small yu + FF D18F; # small ya +} diff --git a/services/etc/tor/torrc b/services/etc/tor/torrc new file mode 100644 index 0000000..37189e3 --- /dev/null +++ b/services/etc/tor/torrc @@ -0,0 +1,11 @@ +ORPort 0 +SocksPort 0 +SocksPolicy reject * +RunAsDaemon 0 +DataDirectory /var/lib/tor/ +Exitpolicy reject *:* +Nickname ExampleName +ContactInfo The Webmaster + +HiddenServiceDir /var/lib/tor/web/ +HiddenServicePort 80 nginx:80 diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..15193a9 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +001-upstreams.bats diff --git a/tests/000-networking.bats b/tests/000-networking.bats new file mode 100644 index 0000000..4e1dfbe --- /dev/null +++ b/tests/000-networking.bats @@ -0,0 +1,14 @@ +# +# basic networking tests +# a reality check, of sorts +# + +@test "[$HOSTNAME][general] networking: is the outside world accessible via IPv4?" { + run ping -c 2 -w 3 8.8.8.8 + [ "$status" -eq 0 ] +} + +@test "[$HOSTNAME][general] networking: is the outside world accessible via IPv6?" { + run ping6 -c 2 -w 3 ipv6.google.com + [ "$status" -eq 0 ] +} diff --git a/tests/gentests.sh b/tests/gentests.sh new file mode 100755 index 0000000..85346f2 --- /dev/null +++ b/tests/gentests.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# +# generating relevant tests +# +# $1 -- path of the upstreams.conf file to use + +# +# first, upstream-related tests +# + +FASADA_TESTS_DIR="$( dirname "$BASH_SOURCE" )" +FASADA_TESTS_UPSTREAMS_FILE="$FASADA_TESTS_DIR/001-upstreams.bats" + +# clear the playing field +echo > "$FASADA_TESTS_UPSTREAMS_FILE" + +# do we have *any* upstreams configured? +cat <> "$FASADA_TESTS_UPSTREAMS_FILE" +@test "[\$HOSTNAME][upstreams] any upstreams configured?" { + [ $( awk '/^upstream/,/^}/' "$1" | egrep '^\s+server' | sed -r -e 's/^\s*server\s([][a-f0-9\.\:]+)(;|\s+.+)/\1/g' | wc -l ) != 0 ] +} +EOF + +# ok, let's go through hte upstreams +# and generate tests per-upstream +for upstream in $( awk '/^upstream/,/^}/' "$1" | egrep '^\s+server' | sed -r -e 's/^\s*server\s([][a-f0-9\.\:]+)(;|\s+.+)/\1/g' ); do + + # NOTICE: this will also handle regular domain names... not sure if that's what we want + if [[ "$upstream" = *'.'* ]]; then + # IPv4 + BATS_RUN="run ping -c 2 -w 3 '${upstream%:*}'" + else + # IPv6 + BATS_RUN="run ping6 -c 2 -w 3 '$( echo -n ${upstream%]*} | tr -d '[]' )'" + fi + + cat <> "$FASADA_TESTS_UPSTREAMS_FILE" +# pinging the upstream but *without* the port, obviously +@test "[\$HOSTNAME][upstreams] testing upstream: $upstream - accessible via ping?" { + $BATS_RUN + [ "\$status" -eq 0 ] +} +EOF + + # checking TCP connectivity + # yes we're using bash built-in /dev/tcp for this to not rely on things like curl or wget + # relevant: https://www.linuxjournal.com/content/more-using-bashs-built-devtcp-file-tcpip + # + # no need to get *too* fancy here, if a HTTP request is sent to a HTTPS port + # we will still get a HTTP/1.1 Bad Request plain text response + # + # if no port is specified, default to 80 + # using example.com for all requests, we just want to check if a webserver is listening + # are we doing IPv4 or IPv6? + if [[ $upstream = *'.'* ]]; then + # IPv4 (or a domain name), we can assume there's max. a single ':' + UPSTREAM_IP="${upstream%:*}" + UPSTREAM_PORT="${upstream#*:}" + if [ "$UPSTREAM_IP" == "$UPSTREAM_PORT" ]; then + UPSTREAM_PORT="80" + fi + else + # IPv6, square brackets are obligatory, otherwise nginx complains about invalid port + # so we can use that to drop the port if any + UPSTREAM_IP="$( echo -n ${upstream%]*} | tr -d '[]' )" + UPSTREAM_PORT="${upstream#*]:}" + if [ "$UPSTREAM_PORT" == "" ]; then + UPSTREAM_PORT="80" + fi + fi + +cat <> "$FASADA_TESTS_UPSTREAMS_FILE" +@test "[\$HOSTNAME][upstreams] testing upstream: $upstream - accessible via HTTP/HTTPS?" { + exec 8<>/dev/tcp/$UPSTREAM_IP/$UPSTREAM_PORT + echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" >&8 + run timeout 3 cat <&8 + exec 8<&- + [[ "\${lines[0]}" == "HTTP/1.1 "* ]] +} + +EOF +done diff --git a/tests/runtests.sh b/tests/runtests.sh new file mode 100755 index 0000000..76fcf0b --- /dev/null +++ b/tests/runtests.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# +# running the tests +# +# this should be ran directly on the bare metal +# *not* in the docker container +# +# it will generate the tests and then run them, +# first on bare metal, and then in the nginx container +# + +# get the source directory +FASADA_TESTS_DIR="$( dirname "$BASH_SOURCE" )" + +# generate the tests +# +# ...but only if we're not currently running the tests inside the container +if [ "$1" != '--running-in-container' ]; then + "$FASADA_TESTS_DIR"/gentests.sh "$FASADA_TESTS_DIR/../services/etc/nginx/conf.d/upstreams.conf" +fi + +# make sure PATH is set to what we need +export PATH="$FASADA_TESTS_DIR/bats-core/bin:$PATH" + +# do the bagic +bats "$FASADA_TESTS_DIR"/*.bats + +# do the magic in the container +# +# ...unless we are already running in the container +if [ "$1" != '--running-in-container' ]; then + cd "$FASADA_TESTS_DIR/../" + docker-compose exec nginx /opt/tests/runtests.sh --running-in-container +fi