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 @@ -