From 7e6612467f5b72528b65f5e352a68f62b3dc09ef Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 18 May 2023 17:03:35 +0200 Subject: [PATCH] add modsec Signed-off-by: Zoey Update Dockerfile --- Dockerfile | 50 +++++- README.md | 7 +- backend/doc/api.swagger.json | 2 +- backend/internal/certificate.js | 143 ++++++------------ backend/internal/nginx.js | 2 +- backend/templates/proxy_host.conf | 9 ++ compose.yaml | 4 +- frontend/js/app/nginx/proxy/form.ejs | 6 +- frontend/js/i18n/messages.json | 4 +- frontend/package.json | 2 +- rootfs/bin/start.sh | 5 + .../conf/conf.d/include/block-exploits.conf | 28 ++++ rootfs/usr/local/nginx/conf/conf.d/npm.conf | 3 + 13 files changed, 151 insertions(+), 114 deletions(-) diff --git a/Dockerfile b/Dockerfile index 885b153a..ec2cdf71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,12 +28,14 @@ RUN apk add --no-cache ca-certificates nodejs-current yarn && \ node-prune && \ yarn cache clean --all + FROM python:3.11.3-alpine3.18 as certbot RUN apk add --no-cache ca-certificates build-base libffi-dev && \ python3 -m venv /usr/local/certbot && \ . /usr/local/certbot/bin/activate && \ pip install --no-cache-dir certbot + FROM --platform="$BUILDPLATFORM" alpine:3.18.0 as crowdsec RUN apk add --no-cache ca-certificates git build-base && \ git clone --recursive https://github.com/crowdsecurity/cs-nginx-bouncer /src && \ @@ -50,17 +52,55 @@ RUN apk add --no-cache ca-certificates git build-base && \ sed -i "s|BAN_TEMPLATE_PATH=.*|BAN_TEMPLATE_PATH=/data/etc/crowdsec/ban.html|g" lua-mod/config_example.conf && \ sed -i "s|CAPTCHA_TEMPLATE_PATH=.*|CAPTCHA_TEMPLATE_PATH=/data/etc/crowdsec/crowdsec.conf|g" lua-mod/config_example.conf -FROM zoeyvid/nginx-quic:126 + +FROM zoeyvid/nginx-quic:142 +COPY rootfs / RUN apk add --no-cache ca-certificates tzdata \ + lua5.1-lzlib \ nodejs-current \ - luarocks5.1 wget lua5.1-dev build-base \ openssl apache2-utils \ - coreutils grep jq curl shadow sudo && \ + coreutils grep jq curl shadow sudo \ + luarocks5.1 wget lua5.1-dev build-base git && \ + wget https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended -O /usr/local/nginx/conf/conf.d/include/modsecurity.conf && \ + wget https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/unicode.mapping -O /usr/local/nginx/conf/conf.d/include/unicode.mapping && \ + sed -i "s|SecRuleEngine .*|SecRuleEngine On|g" /usr/local/nginx/conf/conf.d/include/modsecurity.conf && \ + echo "Include /data/etc/modsecurity/modsecurity.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity.conf && \ + cp /usr/local/nginx/conf/conf.d/include/modsecurity.conf /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + echo "Include /data/etc/modsecurity/crs-setup.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + echo "Include /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + echo "#Include /usr/local/nginx/conf/conf.d/include/coreruleset/plugins/*-config.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + echo "#Include /usr/local/nginx/conf/conf.d/include/coreruleset/plugins/*-before.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + echo "Include /usr/local/nginx/conf/conf.d/include/coreruleset/rules/*.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + echo "#Include /usr/local/nginx/conf/conf.d/include/coreruleset/plugins/*-after.conf" | tee -a /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf && \ + git clone https://github.com/coreruleset/coreruleset /tmp/coreruleset && \ + wget https://patch-diff.githubusercontent.com/raw/coreruleset/coreruleset/pull/3218.patch -O /tmp/coreruleset/http3.patch && \ + cd /tmp/coreruleset && \ + git apply /tmp/coreruleset/http3.patch && \ + mkdir /usr/local/nginx/conf/conf.d/include/coreruleset && \ + cp /tmp/coreruleset/crs-setup.conf.example /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf.example && \ + sed -i '/#/!d' /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf.example && \ + mv /tmp/coreruleset/crs-setup.conf.example /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf && \ + mv /tmp/coreruleset/rules /usr/local/nginx/conf/conf.d/include/coreruleset/rules && \ + git clone --recursive https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin /tmp/phpmyadmin-rule-exclusions-plugin && \ + git clone --recursive https://github.com/coreruleset/nextcloud-rule-exclusions-plugin /tmp/nextcloud-rule-exclusions-plugin && \ + git clone --recursive https://github.com/coreruleset/wordpress-rule-exclusions-plugin /tmp/wordpress-rule-exclusions-plugin && \ + git clone --recursive https://github.com/coreruleset/cpanel-rule-exclusions-plugin /tmp/cpanel-rule-exclusions-plugin && \ + git clone --recursive https://github.com/coreruleset/body-decompress-plugin /tmp/body-decompress-plugin && \ + git clone --recursive https://github.com/coreruleset/auto-decoding-plugin /tmp/auto-decoding-plugin && \ + git clone --recursive https://github.com/coreruleset/google-oauth2-plugin /tmp/google-oauth2-plugin && \ + mv /tmp/coreruleset/plugins /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/phpmyadmin-rule-exclusions-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/nextcloud-rule-exclusions-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/wordpress-rule-exclusions-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/cpanel-rule-exclusions-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/body-decompress-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/auto-decoding-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + mv /tmp/google-oauth2-plugin/plugins/* /usr/local/nginx/conf/conf.d/include/coreruleset/plugins && \ + rm -r /tmp/* && \ luarocks-5.1 install lua-resty-http && \ luarocks-5.1 install lua-cjson && \ - apk del --no-cache luarocks5.1 wget lua5.1-dev build-base + apk del --no-cache luarocks5.1 wget lua5.1-dev build-base git -COPY rootfs / COPY --from=backend /build/backend /app COPY --from=frontend /build/frontend/dist /app/frontend COPY --from=certbot /usr/local/certbot /usr/local/certbot diff --git a/README.md b/README.md index 9e909527..e0f04dc4 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,9 @@ so that the barrier for entry here is low. # List of new features -- Supports HTTP/3 (QUIC) protocol +- Supports HTTP/3 (QUIC) protocol aviable - Supports Crowdsec. Please read below for instructions on how to use it. +- Supports ModSecurity, with coreruleset as an option. You can configure ModSecurity/coreruleset by editing the files in the `/opt/npm/etc/modsecurity` folder. - Darkmode button in the footer for comfortable viewing - Fixes proxy to https origin when the origin only accepts TLSv1.3 - Only enables TLSv1.2 and TLSv1.3 protocols @@ -178,9 +179,9 @@ services: # - "CLEAN=false" # Clean folders, default true # - "FULLCLEAN=true" # Clean unused config folders, default false # - "PHP81=true" # Activate PHP81, default false -# - "PHP81_APKS=php81-curl php-81-curl" # Add php extensions, see aviable packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php81-*, default none +# - "PHP81_APKS=php81-curl php-81-curl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php81-*, default none # - "PHP82=true" # Activate PHP82, default false -# - "PHP82_APKS=php82-curl php-82-curl" # Add php extensions, see aviable packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php82-*, default none +# - "PHP82_APKS=php82-curl php-82-curl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php82-*, default none ``` 3. Bring up your stack by running (or deploy your portainer stack) diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json index 596642bd..13045598 100644 --- a/backend/doc/api.swagger.json +++ b/backend/doc/api.swagger.json @@ -87,7 +87,7 @@ "letsencrypt_agree": false, "dns_challenge": false, "nginx_online": false, - "nginx_err": "Command failed: /usr/sbin/nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /etc/nginx/nginx.conf test failed\n" + "nginx_err": "Command failed: nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /urs/local/nginx/conf/nginx.conf test failed\n" }, "allow_websocket_upgrade": 0, "http2_support": 0, diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index f9a94e60..4a562748 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -10,7 +10,6 @@ const certificateModel = require('../models/certificate'); const dnsPlugins = require('../certbot-dns-plugins'); const internalAuditLog = require('./audit-log'); const internalNginx = require('./nginx'); -const internalHost = require('./host'); const archiver = require('archiver'); const path = require('path'); const { isArray } = require('lodash'); @@ -126,112 +125,65 @@ const internalCertificate = { }) .then((certificate) => { if (certificate.provider === 'letsencrypt') { - // Request a new Cert using Certbot. Let the fun begin. - - // 1. Find out any hosts that are using any of the hostnames in this cert - // 2. Disable them in nginx temporarily - // 3. Generate the Certbot config - // 4. Request cert - // 5. Remove Certbot config - // 6. Re-instate previously disabled hosts - - // 1. Find out any hosts that are using any of the hostnames in this cert - return internalHost.getHostsWithDomains(certificate.domain_names) - .then((in_use_result) => { - // 2. Disable them in nginx temporarily - return internalCertificate.disableInUseHosts(in_use_result) - .then(() => { - return in_use_result; - }); - }) - .then((in_use_result) => { - // With DNS challenge no config is needed, so skip 3 and 5. - if (certificate.meta.dns_challenge) { - return internalNginx.reload().then(() => { - // 4. Request cert - return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate); + // Request a new Cert using Certbot. Let the fun begin. + if (certificate.meta.dns_challenge) { + return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate) + .then(() => { + return certificate; + }) + .catch((err) => { + // In the event of failure, throw err back + throw err; + }); + } else { + return internalCertificate.requestLetsEncryptSsl(certificate) + .then(() => { + return certificate; + }) + .catch((err) => { + // In the event of failure, throw err back + throw err; + }); + } + } else { + return certificate; + } + }) + .then((certificate) => { + if (certificate.provider === 'letsencrypt') { + // At this point, the certbot cert should exist on disk. + // Lets get the expiry date from the file and update the row silently + return internalCertificate + .getCertificateInfoFromFile( + '/data/tls/certbot/live/npm-' + certificate.id + '/fullchain.pem' + ) + .then((cert_info) => { + return certificateModel + .query() + .patchAndFetchById(certificate.id, { + expires_on: moment(cert_info.dates.to, 'X').format( + 'YYYY-MM-DD HH:mm:ss' + ), }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalCertificate.enableInUseHosts(in_use_result) - .then(internalNginx.reload) - .then(() => { - throw err; - }); + .then((saved_row) => { + // Add cert data for audit log + saved_row.meta = _.assign({}, saved_row.meta, { + letsencrypt_certificate: cert_info, }); - } else { - // 3. Generate the Certbot config - return internalNginx.generateLetsEncryptRequestConfig(certificate) - .then(internalNginx.reload) - .then(async() => await new Promise((r) => setTimeout(r, 5000))) - .then(() => { - // 4. Request cert - return internalCertificate.requestLetsEncryptSsl(certificate); - }) - .then(() => { - // 5. Remove Certbot config - return internalNginx.deleteLetsEncryptRequestConfig(certificate); - }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalNginx.deleteLetsEncryptRequestConfig(certificate) - .then(() => { - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(internalNginx.reload) - .then(() => { - throw err; - }); - }); - } - }) - .then(() => { - // At this point, the certbot cert should exist on disk. - // Lets get the expiry date from the file and update the row silently - return internalCertificate.getCertificateInfoFromFile('/data/tls/certbot/live/npm-' + certificate.id + '/fullchain.pem') - .then((cert_info) => { - return certificateModel - .query() - .patchAndFetchById(certificate.id, { - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }) - .then((saved_row) => { - // Add cert data for audit log - saved_row.meta = _.assign({}, saved_row.meta, { - letsencrypt_certificate: cert_info - }); - return saved_row; - }); + return saved_row; }); }).catch(async (error) => { // Delete the certificate from the database if it was not created successfully - await certificateModel - .query() - .deleteById(certificate.id); + await certificateModel.query().deleteById(certificate.id); throw error; }); } else { return certificate; } - }).then((certificate) => { + }) + .then((certificate) => { data.meta = _.assign({}, data.meta || {}, certificate.meta); @@ -248,6 +200,7 @@ const internalCertificate = { }); }, + /** * @param {Access} access * @param {Object} data diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 04bf421b..660635e0 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -115,7 +115,7 @@ const internalNginx = { return internalNginx.test() .then(() => { logger.info('Reloading Nginx'); - return utils.exec('nginx -s reload'); + return utils.exec('kill $(cat /usr/local/nginx/logs/nginx.pid); nginx'); }); }, diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index ecaf15a7..f205d541 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -13,6 +13,15 @@ server { {% include "_brotli.conf" %} {% include "_access.conf" %} + {% if block_exploits == 1 or block_exploits == true %} + modsecurity on; + {% if caching_enabled == 1 or caching_enabled == true -%} + modsecurity_rules_file /usr/local/nginx/conf/conf.d/include/modsecurity-crs.conf; + {% else %} + modsecurity_rules_file /usr/local/nginx/conf/conf.d/include/modsecurity.conf; + {% endif %} + {% endif %} + include conf.d/include/acme-challenge.conf; include conf.d/include/block-exploits.conf; diff --git a/compose.yaml b/compose.yaml index 7d2b0114..e56db3b4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -28,6 +28,6 @@ services: # - "CLEAN=false" # Clean folders, default true # - "FULLCLEAN=true" # Clean unused config folders, default false # - "PHP81=true" # Activate PHP81, default false -# - "PHP81_APKS=php81-curl php-81-curl" # Add php extensions, see aviable packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php81-*, default none +# - "PHP81_APKS=php81-curl php-81-curl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php81-*, default none # - "PHP82=true" # Activate PHP82, default false -# - "PHP82_APKS=php82-curl php-82-curl" # Add php extensions, see aviable packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php82-*, default none +# - "PHP82_APKS=php82-curl php-82-curl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.17&repo=community&arch=x86_64&name=php82-*, default none diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 7182ffb6..32565754 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -54,7 +54,7 @@ -
+
-
diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 28b7831a..f3aa257f 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -75,8 +75,8 @@ "http2-support": "Enable Brotli", "domain-names": "Domain Names", "cert-provider": "Certificate Provider", - "block-exploits": "Block Common Exploits", - "caching-enabled": "Cache Assets", + "block-exploits": "Enable ModSecurity", + "caching-enabled": "Enable CoreRuleSet (Requires ModSecurity)", "ssl-certificate": "TLS Certificate", "none": "None", "new-cert": "Request a new TLS Certificate", diff --git a/frontend/package.json b/frontend/package.json index 65cb29fc..75f4b13c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "nodemon": "2.0.22", "numeral": "2.0.6", "sass-loader": "10.4.1", - "style-loader": "3.3.2", + "style-loader": "3.3.3", "tabler-ui": "git+https://github.com/tabler/tabler.git#00f78ad823311bc3ad974ac3e5b0126198f0a813", "underscore": "1.13.6", "webpack": "4.46.0", diff --git a/rootfs/bin/start.sh b/rootfs/bin/start.sh index 8cf45c5e..b464307c 100755 --- a/rootfs/bin/start.sh +++ b/rootfs/bin/start.sh @@ -188,6 +188,7 @@ mkdir -vp /data/tls/certbot/renewal \ /data/etc/html \ /data/etc/access \ /data/etc/crowdsec \ + /data/etc/modsecurity \ /data/nginx/redirection_host \ /data/nginx/proxy_host \ /data/nginx/dead_host \ @@ -318,6 +319,7 @@ find /data/nginx -type f -name '*.conf' -exec sed -i "/ssl_stapling/d" {} \; find /data/nginx -type f -name '*.conf' -exec sed -i "/ssl_stapling_verify/d" {} \; touch /data/etc/html/index.html \ + /data/etc/modsecurity/modsecurity.conf \ /data/nginx/default.conf \ /data/nginx/ip_ranges.conf \ /data/nginx/custom/root.conf \ @@ -332,6 +334,9 @@ touch /data/etc/html/index.html \ /data/nginx/custom/server_stream_tcp.conf \ /data/nginx/custom/server_stream_udp.conf +cp -vn /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf.example /data/etc/modsecurity/crs-setup.conf +cp -v /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf.example /data/etc/modsecurity/crs-setup.conf.example + if [ -z "$NPM_CERT_ID" ]; then export NPM_CERT=/data/tls/dummycert.pem export NPM_KEY=/data/tls/dummykey.pem diff --git a/rootfs/usr/local/nginx/conf/conf.d/include/block-exploits.conf b/rootfs/usr/local/nginx/conf/conf.d/include/block-exploits.conf index 22360fc1..3b15e06f 100644 --- a/rootfs/usr/local/nginx/conf/conf.d/include/block-exploits.conf +++ b/rootfs/usr/local/nginx/conf/conf.d/include/block-exploits.conf @@ -131,6 +131,34 @@ if ($http_user_agent ~ "GrabNet") { set $block_user_agents 1; } +if ($http_user_agent ~ "Amazonbot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Applebot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Bingbot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Facebookbot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Googlebot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "LinkedInBot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Twitterbot") { + set $block_user_agents 1; +} + if ($block_user_agents = 1) { return 403; } diff --git a/rootfs/usr/local/nginx/conf/conf.d/npm.conf b/rootfs/usr/local/nginx/conf/conf.d/npm.conf index 5a2bb8dc..73424012 100644 --- a/rootfs/usr/local/nginx/conf/conf.d/npm.conf +++ b/rootfs/usr/local/nginx/conf/conf.d/npm.conf @@ -12,6 +12,9 @@ server { include conf.d/include/force-tls.conf; include conf.d/include/tls-ciphers.conf; include conf.d/include/block-exploits.conf; + + modsecurity on; + modsecurity_rules_file /usr/local/nginx/conf/conf.d/include/modsecurity.conf; #ssl_certificate ; #ssl_certificate_key ;