diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a72788fb508c319c6594c169b617c92fc9a31bb5..62ca6edd9b78ba115ebb35736d5f472130db5e5c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -151,3 +151,19 @@ push-webdav:
   needs:
     - job: build-webdav
       artifacts: true
+
+# nginx image:
+
+build-nginx:
+  extends: .container-build-base
+  variables:
+    CONTEXT: nginx
+
+push-nginx:
+  extends: .container-push-base
+  variables:
+    CONTEXT: nginx
+    IMAGE: nginx
+  needs:
+    - job: build-nginx
+      artifacts: true
diff --git a/nginx/.dockerignore b/nginx/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..380e2e62d48d3718eee6fb713bc578042f0ab6fd
--- /dev/null
+++ b/nginx/.dockerignore
@@ -0,0 +1,4 @@
+*
+!conf.d/
+!www/
+!snippets/
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..41618e0a01a69691c02e87e6dc8fa0ba4f07b8a2
--- /dev/null
+++ b/nginx/Dockerfile
@@ -0,0 +1,26 @@
+FROM nginx:1.21.3-alpine
+# hadolint ignore=DL3018
+RUN rm -rf /etc/nginx/conf./* && \
+    chmod 777 /run && \
+    apk add --no-cache --update libcap openssl && \
+    curl https://letsencrypt.org/certs/isrg-root-ocsp-x1.pem.txt > /etc/ssl/ocsp.pem && \
+    mkdir /var/ssl &&\
+    curl https://ssl-config.mozilla.org/ffdhe2048.txt > /var/ssl/dhparams &&\
+    chmod 644 /var/ssl/dhparams && \
+    install -d -m 755 -o root -g root /etc/nginx/snippets && \
+    install -d -m 755 -o root -g root /var/ssl && \
+    install -d -m 755 -o root -g root /var/www && \
+    install -d -m 700 -o nginx -g nginx /var/cache/nginx && \
+    openssl req -x509 \
+                -newkey rsa:4096 \
+                -keyout /var/ssl/site.key \
+                -nodes \
+                -out /var/ssl/site.crt \
+                -batch && \
+    setcap CAP_NET_BIND_SERVICE=+ep "$(command -v nginx)" && \
+    chown nginx /var/ssl/site.*
+COPY conf.d/ /etc/nginx/conf.d/
+COPY snippets/ /etc/nginx/snippets/
+USER nginx
+RUN nginx -t
+HEALTHCHECK CMD curl --fail --verbose --user-agent 'Docker health check' --header "Host: status" http://localhost/ || exit 1
diff --git a/nginx/README.md b/nginx/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..630b37a377e6609d2bf5eb1bcffd937041601eee
--- /dev/null
+++ b/nginx/README.md
@@ -0,0 +1,3 @@
+# Nginx
+
+My tweaked version of the Nginx image.
diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf
new file mode 100644
index 0000000000000000000000000000000000000000..f428ba9fdb62cac0a123ea3c6832ab5d29d1c4e3
--- /dev/null
+++ b/nginx/conf.d/default.conf
@@ -0,0 +1,13 @@
+server {
+    listen      80 default_server;
+    listen      [::]:80 default_server;
+    include     snippets/www-acme-challenge.conf;
+    location    / { return 301 https://www.shore.co.il$request_uri; }
+}
+
+server {
+    listen      443 ssl http2 default_server;
+    listen      [::]:443 ssl http2 default_server;
+    include     snippets/ssl.conf;
+    location    / { return 301 https://www.shore.co.il$request_uri; }
+}
diff --git a/nginx/conf.d/global.conf b/nginx/conf.d/global.conf
new file mode 100644
index 0000000000000000000000000000000000000000..608fe8de67213f080cf2736ceceab71c26117be1
--- /dev/null
+++ b/nginx/conf.d/global.conf
@@ -0,0 +1,13 @@
+# The resolver for the Docker network.
+resolver                        127.0.0.11 valid=30s;
+gzip                            on;
+tcp_nopush                      on;
+tcp_nodelay                     on;
+server_tokens                   off;
+include                         snippets/common-headers.conf;
+# Validate proxied SSL connections.
+proxy_ssl_trusted_certificate   /etc/ssl/certs/ca-certificates.crt;
+proxy_ssl_verify                on;
+proxy_ssl_verify_depth          4;
+# For proxying /validate on different hosts to Vouch.
+map $host $vouch { default vouch; }
diff --git a/nginx/conf.d/status.conf b/nginx/conf.d/status.conf
new file mode 100644
index 0000000000000000000000000000000000000000..6ecb7d85dd156109d327ee8d4b3e0ba39ce1681d
--- /dev/null
+++ b/nginx/conf.d/status.conf
@@ -0,0 +1,7 @@
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name status;
+    location = /        { stub_status; }
+    include     snippets/allow-private-ips.conf;
+}
diff --git a/nginx/snippets/ads-txt.conf b/nginx/snippets/ads-txt.conf
new file mode 100644
index 0000000000000000000000000000000000000000..b074c08328eef2bc8a18f41937ff787ceacbe6ba
--- /dev/null
+++ b/nginx/snippets/ads-txt.conf
@@ -0,0 +1,9 @@
+location = /ads.txt {
+    if ($scheme = http) {
+        return 301 https://$host$request_uri;
+    }
+    if ($scheme = https) {
+        add_header Content-Type "text/plain; charset=utf-8";
+        return 200 "contact=webmaster@shore.co.il\n";
+    }
+}
diff --git a/nginx/snippets/allow-ns1.conf b/nginx/snippets/allow-ns1.conf
new file mode 100644
index 0000000000000000000000000000000000000000..bdadb248d461af214acec72962ed6f45b9ac4651
--- /dev/null
+++ b/nginx/snippets/allow-ns1.conf
@@ -0,0 +1 @@
+allow   62.219.131.121;  # ns1.shore.co.il
diff --git a/nginx/snippets/allow-ns4.conf b/nginx/snippets/allow-ns4.conf
new file mode 100644
index 0000000000000000000000000000000000000000..5e39f4028d30aa2529179de757b07a19d4039ff6
--- /dev/null
+++ b/nginx/snippets/allow-ns4.conf
@@ -0,0 +1 @@
+allow   163.172.74.36;  # ns4.shore.co.il
diff --git a/nginx/snippets/allow-private-ips.conf b/nginx/snippets/allow-private-ips.conf
new file mode 100644
index 0000000000000000000000000000000000000000..154262aa4070edf80c878a8fba8cdf6a9f03030a
--- /dev/null
+++ b/nginx/snippets/allow-private-ips.conf
@@ -0,0 +1,5 @@
+allow 127.0.0.0/8;
+allow 10.0.0.0/8;
+allow 192.168.0.0/16;
+allow 172.16.0.0/12;
+deny all;
diff --git a/nginx/snippets/allow-shore-ips.conf b/nginx/snippets/allow-shore-ips.conf
new file mode 100644
index 0000000000000000000000000000000000000000..709b549d2e1c5e15fa5cd4c8d671a509181f6a0f
--- /dev/null
+++ b/nginx/snippets/allow-shore-ips.conf
@@ -0,0 +1,3 @@
+include snippets/allow-ns1.conf;
+include snippets/allow-ns4.conf;
+include snippets/allow-private-ips.conf;
diff --git a/nginx/snippets/common-headers.conf b/nginx/snippets/common-headers.conf
new file mode 100644
index 0000000000000000000000000000000000000000..e97cb6890f107423095a68af45db2c4662ecc482
--- /dev/null
+++ b/nginx/snippets/common-headers.conf
@@ -0,0 +1,6 @@
+# add_headers are inherited from previous level if and only if there are no
+# add_header directives defined on the current level. So any time there's an
+# add_header directive there should be an `include snippets/common-headers.conf`
+# directive as well.
+add_header      X-Frame-Options SAMEORIGIN always;
+add_header      Permissions-Policy interest-cohort=();
diff --git a/nginx/snippets/ldap-auth.conf b/nginx/snippets/ldap-auth.conf
new file mode 100644
index 0000000000000000000000000000000000000000..822c4407093249d3d77133c9e93b86374b2afae9
--- /dev/null
+++ b/nginx/snippets/ldap-auth.conf
@@ -0,0 +1,10 @@
+auth_request    /validate;
+
+location = /validate {
+  proxy_pass                        https://auth.shore.co.il/validate;
+  proxy_http_version                1.1;
+  include                           snippets/proxy-ssl.conf;
+  internal;
+  proxy_pass_request_body           off;
+  proxy_set_header Content-Length   "";
+}
diff --git a/nginx/snippets/proxy-headers.conf b/nginx/snippets/proxy-headers.conf
new file mode 100644
index 0000000000000000000000000000000000000000..e1420368822afd1ec4d574b95a2403909fe2a439
--- /dev/null
+++ b/nginx/snippets/proxy-headers.conf
@@ -0,0 +1,8 @@
+proxy_set_header    X-Forwarded-Host $host;
+proxy_set_header    X-Forwarded-Proto $scheme;
+proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
+proxy_set_header    Host $host;
+proxy_set_header    X-Real-IP $remote_addr;
+proxy_hide_header   Strict-Transport-Security;
+proxy_hide_header   Public-Key-Pins;
+proxy_hide_header   Public-Key-Pins-Report-Only;
diff --git a/nginx/snippets/proxy-ssl.conf b/nginx/snippets/proxy-ssl.conf
new file mode 100644
index 0000000000000000000000000000000000000000..b83886af06e69be66442d924d1ca1c2f58c88125
--- /dev/null
+++ b/nginx/snippets/proxy-ssl.conf
@@ -0,0 +1,5 @@
+proxy_ssl_verify                  on;
+proxy_ssl_verify_depth            3;
+proxy_ssl_name                    auth.shore.co.il;
+proxy_ssl_server_name             on;
+proxy_ssl_trusted_certificate     /etc/ssl/certs/ca-certificates.crt;
diff --git a/nginx/snippets/redirect-https.conf b/nginx/snippets/redirect-https.conf
new file mode 100644
index 0000000000000000000000000000000000000000..991d5934ea26fdf1596be7731bca044be57e1a21
--- /dev/null
+++ b/nginx/snippets/redirect-https.conf
@@ -0,0 +1 @@
+location    / { return 301 https://$host$request_uri; }
diff --git a/nginx/snippets/redirect-www.conf b/nginx/snippets/redirect-www.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2d89d75e34296121d630cd90330793dff97ff19d
--- /dev/null
+++ b/nginx/snippets/redirect-www.conf
@@ -0,0 +1 @@
+location    / { return 301 https://www.$host$request_uri; }
diff --git a/nginx/snippets/robots-allow-all.conf b/nginx/snippets/robots-allow-all.conf
new file mode 100644
index 0000000000000000000000000000000000000000..627aee5db300861870c3dc70c82016a4abd676c2
--- /dev/null
+++ b/nginx/snippets/robots-allow-all.conf
@@ -0,0 +1,4 @@
+location = /robots.txt {
+    add_header Content-Type "text/plain; charset=utf-8";
+    return 200 "User-agent: *\nDisallow:\n";
+}
diff --git a/nginx/snippets/robots-disallow-all.conf b/nginx/snippets/robots-disallow-all.conf
new file mode 100644
index 0000000000000000000000000000000000000000..03d50312dfd4108ef9758ff8d65f4d090d0a4c1c
--- /dev/null
+++ b/nginx/snippets/robots-disallow-all.conf
@@ -0,0 +1,4 @@
+location = /robots.txt {
+    add_header Content-Type "text/plain; charset=utf-8";
+    return 200 "User-agent: *\nDisallow: *\n";
+}
diff --git a/nginx/snippets/security-txt.conf b/nginx/snippets/security-txt.conf
new file mode 100644
index 0000000000000000000000000000000000000000..c1f0d219b5dc1f4fe2537633a0c2ed58f05f10b3
--- /dev/null
+++ b/nginx/snippets/security-txt.conf
@@ -0,0 +1,9 @@
+location = /.well-known/security.txt {
+    if ($scheme = http) {
+        return 301 https://$host$request_uri;
+    }
+    if ($scheme = https) {
+        add_header Content-Type "text/plain; charset=utf-8";
+        return 200 "Contact: mailto:security@shore.co.il\nEncryption: https://www.shore.co.il/blog/static/nimrod.asc";
+    }
+}
diff --git a/nginx/snippets/ssl.conf b/nginx/snippets/ssl.conf
new file mode 100644
index 0000000000000000000000000000000000000000..cb1f77f67c32f78cacdde5ed8f5f0d74b346ac2c
--- /dev/null
+++ b/nginx/snippets/ssl.conf
@@ -0,0 +1,14 @@
+add_header                  Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
+add_header                  Expect-CT "max-age=86400, enforce, report-uri=\"https://www.shore.co.il/about\"";
+include                     snippets/common-headers.conf;
+ssl_certificate             /var/ssl/site.crt;
+ssl_certificate_key         /var/ssl/site.key;
+ssl_dhparam                 /var/ssl/dhparams;
+ssl_protocols               TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
+ssl_ciphers                 !AESCCM:!kRSA:!3DES:!RC4:!DES:!MD5:!aNULL:!NULL:AESGCM+ECDH:ECDH+CHACHA20:AES256+ECDH:AES128:CHACHA20:+SHA1;
+ssl_prefer_server_ciphers   on;
+ssl_session_cache           shared:SSL:50m;
+ssl_session_timeout         5m;
+ssl_stapling                on;
+ssl_stapling_verify         on;
+ssl_trusted_certificate     /etc/ssl/ocsp.pem;
diff --git a/nginx/snippets/upgrade-secure.conf b/nginx/snippets/upgrade-secure.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2abc805d48d6d33d67fa70967ac85fc7075dc65c
--- /dev/null
+++ b/nginx/snippets/upgrade-secure.conf
@@ -0,0 +1 @@
+if ($http_Upgrade-Insecure-Requests = 1) { return 301 https://$host$request_uri; }
diff --git a/nginx/snippets/vouch.conf b/nginx/snippets/vouch.conf
new file mode 100644
index 0000000000000000000000000000000000000000..9571b80c28f366b99b57096ab7c23afacf61b46d
--- /dev/null
+++ b/nginx/snippets/vouch.conf
@@ -0,0 +1,30 @@
+# send all requests to the `/validate` endpoint for authorization
+auth_request    /validate;
+
+location = /validate {
+  # forward the /validate request to Vouch Proxy
+  proxy_pass                        http://$vouch:9090/validate;
+  proxy_http_version                1.1;
+  internal;
+  include                           snippets/proxy-headers.conf;
+
+  # Vouch Proxy only acts on the request headers
+  proxy_pass_request_body           off;
+  proxy_set_header Content-Length   "";
+
+  # optionally add X-Vouch-User as returned by Vouch Proxy along with the request
+  auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
+
+  # these return values are used by the @error401 call
+  auth_request_set                  $auth_resp_jwt $upstream_http_x_vouch_jwt;
+  auth_request_set                  $auth_resp_err $upstream_http_x_vouch_err;
+  auth_request_set                  $auth_resp_failcount $upstream_http_x_vouch_failcount;
+}
+
+# if validate returns `401 not authorized` then forward the request to the error401block
+error_page 401 = @error401;
+
+location @error401 {
+    # redirect to Vouch Proxy for login
+    return 302 https://vouch.shore.co.il/login?url=$scheme://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
+}
diff --git a/nginx/snippets/websockets.conf b/nginx/snippets/websockets.conf
new file mode 100644
index 0000000000000000000000000000000000000000..64b7e3736a33c2d1e6621b4b0d64076030759251
--- /dev/null
+++ b/nginx/snippets/websockets.conf
@@ -0,0 +1,3 @@
+proxy_set_header    Upgrade $http_upgrade;
+proxy_set_header    Connection "Upgrade";
+proxy_read_timeout  36000s;
diff --git a/nginx/snippets/www-acme-challenge.conf b/nginx/snippets/www-acme-challenge.conf
new file mode 100644
index 0000000000000000000000000000000000000000..ba3c0b7117cdc522b64ab5593b5d888e72e8a7df
--- /dev/null
+++ b/nginx/snippets/www-acme-challenge.conf
@@ -0,0 +1 @@
+location /.well-known/acme-challenge/ { root /var/www/www.shore.co.il; }