diff --git a/.grype.yaml b/.grype.yaml index 23614b6..04b4903 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -22,3 +22,18 @@ ignore: package: name: curl type: binary + # Same binary-classifier false positive as above, newly published CVE IDs. + # Verified against simplerisk-minimal (php 8.3/8.4/8.5): the only "curl" + # match is /usr/local/lib/php/extensions/.../curl.so reporting the PHP + # version (e.g. 8.5.10) as its own. The real Debian curl package + # (8.14.1-2+deb13u5) is also affected but Debian's tracker marks both IDs + # "wont-fix", so --only-fixed already excludes that (legitimate) match on + # its own; only the misclassified binary match needs ignoring here. + - vulnerability: CVE-2026-19931 + package: + name: curl + type: binary + - vulnerability: CVE-2026-18924 + package: + name: curl + type: binary diff --git a/CLAUDE.md b/CLAUDE.md index 6226b7d..df74b7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,7 +85,7 @@ The entrypoint script handles: - Writing `config.php` by substituting env vars via `sed` - Automatic database provisioning (`DB_SETUP=automatic|automatic-only|manual|delete`) - Headless schema upgrade of an already-installed database (`DB_UPGRADE=automatic|automatic-only`) — runs SimpleRisk's core release-by-release upgrade (`run_database_upgrade_structured`) as the app DB user via `/db-upgrade.php`, emitting the structured per-release JSON to the log; `automatic-only` exits with the upgrade status (used by the EKS release upgrade Job) -- SSL certificate generation (minimal image generates a CA + signed cert; full-stack generates a self-signed cert) +- SSL certificate generation: both images use a self-signed Apache cert, no CA. Full-stack generates it at build time; minimal generates it at container startup (`entrypoint.sh`'s `set_ssl_certificate`, skipped if a cert already exists) so the private key isn't baked into the shared image layer. - Cron setup (`SIMPLERISK_CRON_SETUP` in minimal; always-on in full-stack) - Supervisor start (full-stack) or `apache2-foreground` (minimal) @@ -123,9 +123,12 @@ The entrypoint script handles: - **PRs** trigger `container-validation.yml`: builds all 5 variants (jammy, noble, php83, php84, php85), runs Dockle (Dockerfile linter) and Grype (CVE scanner, severity cutoff: critical, only-fixed), and runs `generator_checks` — the two `test_generate_dockerfile.sh` harnesses that pin the generators' version/source-mode behaviour. - **Release images are built once, then promoted — never rebuilt.** A push to `testing` runs `publish-testing.yml`, which builds both images from the current testing bundle and publishes immutable tags: `simplerisk-minimal` gets `-php83/-php84/-php85` (multi-arch `linux/amd64,linux/arm64`) and `simplerisk` gets `-jammy/-noble` (amd64). Each image's default variant also takes the bare `` and the floating `:testing`. -- **GA is a manual promote, not a build.** After the release merges to `master`, dispatch `promote-latest.yml`. It retags Docker Hub `:latest` to the existing RC digest (`buildx imagetools create`, multi-arch preserved), mirrors the same digests to GHCR cosign-signed, and writes SSM `/simplerisk/customers/image-tag/latest`. Nothing is rebuilt, so the bytes validated in testing are the bytes that ship. A currency guard refuses to promote a version whose digest is not the one `:testing` currently points at. +- **GA promotion fires automatically on merge to `master`, not on manual dispatch.** `promote-latest.yml` triggers on any push to `master` that touches `simplerisk-minimal/Dockerfile` (which carries `ENV version=`). It retags Docker Hub `:latest` to the existing RC digest (`buildx imagetools create`, multi-arch preserved), mirrors the same digests to GHCR cosign-signed, and writes SSM `/simplerisk/customers/image-tag/latest` — nothing is rebuilt. An idempotence guard skips the mutating steps if `:latest` already matches the target digest; `workflow_dispatch` remains available for a manual heal. `create_new_tag.yml` fires on the same push and tags the release. **There is no approval gate between the `testing`→`master` merge and production** — merging is the release. - The reusable workflow files (`*_rw.yml`) are called by the entry-point workflows. +- `testing` requires 1 approving PR review to merge (repo ruleset, not classic branch protection — check via `gh api repos/simplerisk/docker/rulesets`). Admins can bypass with `gh pr merge --admin`. ### Vulnerability ignore list `.grype.yaml` tracks CVEs intentionally ignored (e.g., unfixable at time of release). Update this file when suppressing a new finding, always with a comment explaining why. + +**Recurring false positive (simplerisk-minimal):** Grype's binary classifier misreads the PHP interpreter's version string embedded in `curl.so` as curl's own version, so new curl CVEs periodically get flagged against a nonexistent `curl `. Before assuming a curl CVE finding is real, check `grype -o json | jq` for a match with `type: binary` at path `.../php/extensions/.../curl.so` — if that's the only match (the real Debian `curl` package is a separate, usually-legitimate match), it's this false positive. Add the CVE ID to `.grype.yaml` scoped to `package: {name: curl, type: binary}`. diff --git a/simplerisk-minimal/.testing-version b/simplerisk-minimal/.testing-version index 66c8154..f0fa3e0 100644 --- a/simplerisk-minimal/.testing-version +++ b/simplerisk-minimal/.testing-version @@ -1 +1 @@ -20260909-001 +20260917-001 diff --git a/simplerisk-minimal/Dockerfile b/simplerisk-minimal/Dockerfile index 42b3e79..30f1e36 100644 --- a/simplerisk-minimal/Dockerfile +++ b/simplerisk-minimal/Dockerfile @@ -16,13 +16,13 @@ SHELL [ "/bin/ash", "-eo", "pipefail", "-c" ] # updates feed, then extract -- fail-closed unless PREGA_BUNDLE_FALLBACK allows the # pre-GA path. See common/download_and_verify_bundle.sh. COPY common/download_and_verify_bundle.sh /download_and_verify_bundle.sh -RUN PREGA_BUNDLE_FALLBACK="$PREGA_BUNDLE_FALLBACK" sh /download_and_verify_bundle.sh 20260909-001 +RUN PREGA_BUNDLE_FALLBACK="$PREGA_BUNDLE_FALLBACK" sh /download_and_verify_bundle.sh 20260917-001 FROM php:${php_version}-apache LABEL maintainer="SimpleRisk " -ENV version=20260909-001 +ENV version=20260917-001 WORKDIR /var/www @@ -99,18 +99,15 @@ RUN echo 'upload_max_filesize = 5M' >> /usr/local/etc/php/conf.d/docker-php-uplo echo 'log_errors = On' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \ echo 'error_log = /dev/stderr' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \ echo 'display_errors = Off' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \ -# Create SSL Certificates for Apache SSL - mkdir -p /etc/apache2/ssl/ca /etc/apache2/ssl/simplerisk && \ -# Generate CA - openssl genrsa -out /etc/apache2/ssl/ca/ca.key 4096 && \ - openssl req -x509 -new -nodes -key /etc/apache2/ssl/ca/ca.key -sha256 -days 3650 -out /etc/apache2/ssl/ca/ca.crt -subj "/CN=SimpleRisk CA" && \ -# Generate certs - openssl genrsa -out /etc/apache2/ssl/simplerisk/simplerisk.key 2048 && \ - openssl req -new -key /etc/apache2/ssl/simplerisk/simplerisk.key -out /etc/apache2/ssl/simplerisk/simplerisk.csr -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,DNS:simplerisk,IP:127.0.0.1,IP:0.0.0.0" && \ - openssl x509 -req -days 365 -in /etc/apache2/ssl/simplerisk/simplerisk.csr -CA /etc/apache2/ssl/ca/ca.crt -CAkey /etc/apache2/ssl/ca/ca.key -CAcreateserial -out /etc/apache2/ssl/simplerisk/simplerisk.crt -copy_extensions copyall && \ - cp /etc/apache2/ssl/ca/ca.crt /usr/local/share/ca-certificates/simplerisk.crt && \ - chmod 644 /usr/local/share/ca-certificates/simplerisk.crt && \ - update-ca-certificates && \ +# SSL certificate directory for Apache. The actual key pair is generated at +# container startup by entrypoint.sh (see set_ssl_certificate), not here at +# build time: a key baked into this RUN would be identical in every pulled +# copy of the image and, combined with registering it as a trusted CA, would +# let anyone who pulls the image forge certs the container trusts (HackerOne +# #3764027). No custom CA is created or installed into the system trust +# store -- Apache's cert is self-signed directly, matching the simplerisk +# (non-minimal) image. + mkdir -p /etc/apache2/ssl/simplerisk && \ # Activate Apache modules a2enmod headers rewrite ssl && \ a2enconf security && \ diff --git a/simplerisk-minimal/common/entrypoint.sh b/simplerisk-minimal/common/entrypoint.sh index ee6c210..cf43d40 100644 --- a/simplerisk-minimal/common/entrypoint.sh +++ b/simplerisk-minimal/common/entrypoint.sh @@ -202,6 +202,33 @@ set_csrf_secret(){ [ -n "${SIMPLERISK_CSRF_SECRET:-}" ] && echo "" > "$CSRF_SECRET_PATH"; } +set_ssl_certificate(){ + # Generate Apache's self-signed TLS key pair at container startup rather + # than at image build time. A key baked into the image (the previous + # behavior) is identical in every copy of the image anyone pulls, and was + # additionally being registered as a trusted CA in the container's system + # trust store -- anyone who extracted that key could forge certs the + # container's own outbound curl calls would accept (HackerOne #3764027). + # Generating here means each deployment gets its own key, and idempotence + # (skip if already present) means an existing cert persisted in the + # /etc/apache2/ssl volume survives container restarts/recreation. + local ssl_dir='/etc/apache2/ssl/simplerisk' + local key="$ssl_dir/simplerisk.key" + local crt="$ssl_dir/simplerisk.crt" + + if [ -f "$key" ] && [ -f "$crt" ]; then + return 0 + fi + + print_log "ssl_setup:info" "No Apache TLS certificate found; generating a self-signed one for this container." + exec_cmd "mkdir -p $ssl_dir" "Failed to create SSL certificate directory. Exiting." + # basicConstraints=CA:FALSE is explicit because openssl's own default + # x509 extensions otherwise mark a self-signed leaf cert as CA:TRUE, + # which Apache logs a warning about on every start. + exec_cmd "openssl req -x509 -newkey rsa:2048 -nodes -keyout $key -out $crt -days 365 -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost,DNS:simplerisk,IP:127.0.0.1,IP:0.0.0.0' -addext 'basicConstraints=critical,CA:FALSE' -addext 'keyUsage=critical,digitalSignature,keyEncipherment' -addext 'extendedKeyUsage=serverAuth'" "Failed to generate self-signed TLS certificate. Exiting." + exec_cmd "chmod 600 $key" "Failed to set permissions on TLS private key. Exiting." +} + set_cron(){ # If SIMPLERISK_CRON_SETUP was passed and it is set to disabled if [[ -n "${SIMPLERISK_CRON_SETUP:-}" && "${SIMPLERISK_CRON_SETUP:-}" = disabled* ]]; then @@ -443,6 +470,8 @@ unset_variables() { } _main() { + set_ssl_certificate + # Detect whether the operator has opted into Docker-managed config # provisioning. If no DB env vars are set, leave config.php absent so # SimpleRisk's web installer runs on first request. diff --git a/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf b/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf index 0b55c84..45dbc28 100644 --- a/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf +++ b/simplerisk-minimal/common/etc/apache2/sites-enabled/default-ssl.conf @@ -11,7 +11,6 @@ SSLStrictSNIVHostCheck Off Options -Indexes SSLEngine on - SSLCACertificateFile /etc/apache2/ssl/ca/ca.crt SSLCertificateFile /etc/apache2/ssl/simplerisk/simplerisk.crt SSLCertificateKeyFile /etc/apache2/ssl/simplerisk/simplerisk.key SSLProtocol -all +TLSv1.2 +TLSv1.3 diff --git a/simplerisk-minimal/generate_dockerfile.sh b/simplerisk-minimal/generate_dockerfile.sh index 53ba282..634b366 100755 --- a/simplerisk-minimal/generate_dockerfile.sh +++ b/simplerisk-minimal/generate_dockerfile.sh @@ -144,18 +144,15 @@ RUN echo 'upload_max_filesize = 5M' >> /usr/local/etc/php/conf.d/docker-php-uplo echo 'log_errors = On' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \\ echo 'error_log = /dev/stderr' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \\ echo 'display_errors = Off' >> /usr/local/etc/php/conf.d/docker-php-error_logging.ini && \\ -# Create SSL Certificates for Apache SSL - mkdir -p /etc/apache2/ssl/ca /etc/apache2/ssl/simplerisk && \\ -# Generate CA - openssl genrsa -out /etc/apache2/ssl/ca/ca.key 4096 && \\ - openssl req -x509 -new -nodes -key /etc/apache2/ssl/ca/ca.key -sha256 -days 3650 -out /etc/apache2/ssl/ca/ca.crt -subj "/CN=SimpleRisk CA" && \\ -# Generate certs - openssl genrsa -out /etc/apache2/ssl/simplerisk/simplerisk.key 2048 && \\ - openssl req -new -key /etc/apache2/ssl/simplerisk/simplerisk.key -out /etc/apache2/ssl/simplerisk/simplerisk.csr -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,DNS:simplerisk,IP:127.0.0.1,IP:0.0.0.0" && \\ - openssl x509 -req -days 365 -in /etc/apache2/ssl/simplerisk/simplerisk.csr -CA /etc/apache2/ssl/ca/ca.crt -CAkey /etc/apache2/ssl/ca/ca.key -CAcreateserial -out /etc/apache2/ssl/simplerisk/simplerisk.crt -copy_extensions copyall && \\ - cp /etc/apache2/ssl/ca/ca.crt /usr/local/share/ca-certificates/simplerisk.crt && \\ - chmod 644 /usr/local/share/ca-certificates/simplerisk.crt && \\ - update-ca-certificates && \\ +# SSL certificate directory for Apache. The actual key pair is generated at +# container startup by entrypoint.sh (see set_ssl_certificate), not here at +# build time: a key baked into this RUN would be identical in every pulled +# copy of the image and, combined with registering it as a trusted CA, would +# let anyone who pulls the image forge certs the container trusts (HackerOne +# #3764027). No custom CA is created or installed into the system trust +# store -- Apache's cert is self-signed directly, matching the simplerisk +# (non-minimal) image. + mkdir -p /etc/apache2/ssl/simplerisk && \\ # Activate Apache modules a2enmod headers rewrite ssl && \\ a2enconf security && \\ diff --git a/simplerisk-minimal/stack.yml b/simplerisk-minimal/stack.yml index 4cd9ca1..478ce0c 100644 --- a/simplerisk-minimal/stack.yml +++ b/simplerisk-minimal/stack.yml @@ -8,7 +8,7 @@ services: - DB_SETUP=automatic - DB_SETUP_PASS=simplerisk_setup - SIMPLERISK_DB_HOSTNAME=mysql - image: simplerisk/simplerisk-minimal:20260909-001 + image: simplerisk/simplerisk-minimal:20260917-001 ports: - "80:80" - "443:443" diff --git a/simplerisk/Dockerfile b/simplerisk/Dockerfile index bad78a7..55a7f51 100644 --- a/simplerisk/Dockerfile +++ b/simplerisk/Dockerfile @@ -14,13 +14,13 @@ SHELL [ "/bin/ash", "-eo", "pipefail", "-c" ] # -fsSL on the SQL fetch too: without --fail, curl writes the 404 body into # /simplerisk.sql and the image ships an HTML error page as its schema. COPY common/download_and_verify_bundle.sh /download_and_verify_bundle.sh -RUN PREGA_BUNDLE_FALLBACK="$PREGA_BUNDLE_FALLBACK" sh /download_and_verify_bundle.sh 20260909-001 && \ - curl -fsSL "https://github.com/simplerisk/database/raw/master/simplerisk-$DB_LANG-20260909-001.sql" > /simplerisk.sql +RUN PREGA_BUNDLE_FALLBACK="$PREGA_BUNDLE_FALLBACK" sh /download_and_verify_bundle.sh 20260917-001 && \ + curl -fsSL "https://github.com/simplerisk/database/raw/master/simplerisk-$DB_LANG-20260917-001.sql" > /simplerisk.sql # Using Ubuntu image FROM ubuntu:${ubuntu_version_code} -ENV version=20260909-001 +ENV version=20260917-001 # Maintained by SimpleRisk LABEL maintainer="Simplerisk " @@ -110,7 +110,13 @@ RUN chown -R www-data: /var/www/simplerisk RUN chown -R www-data: /var/log/simplerisk # Setting up cronjob -RUN echo "* * * * * root /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \ +# Runs as www-data, not root: /var/www/simplerisk (including cron.php) is +# chowned to www-data above, so a root cron entry over a www-data-writable +# file would let any web-tier compromise escalate to root on the next tick +# (the same class of bug as CWE-732 / HackerOne #3761952). cron.php only runs +# application-level jobs that already execute as www-data under Apache, so it +# doesn't need root. +RUN echo "* * * * * www-data /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \ chmod 0644 /etc/cron.d/simplerisk-cron RUN echo "0 0 * * * root /usr/sbin/logrotate /etc/logrotate.d/simplerisk.conf > /dev/null 2>&1" >> /etc/cron.d/logrotate-cron && \ chmod 0644 /etc/cron.d/logrotate-cron diff --git a/simplerisk/generate_dockerfile.sh b/simplerisk/generate_dockerfile.sh index 10e347a..4cadb84 100755 --- a/simplerisk/generate_dockerfile.sh +++ b/simplerisk/generate_dockerfile.sh @@ -159,7 +159,13 @@ RUN chown -R www-data: /var/www/simplerisk RUN chown -R www-data: /var/log/simplerisk # Setting up cronjob -RUN echo "* * * * * root /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \\ +# Runs as www-data, not root: /var/www/simplerisk (including cron.php) is +# chowned to www-data above, so a root cron entry over a www-data-writable +# file would let any web-tier compromise escalate to root on the next tick +# (the same class of bug as CWE-732 / HackerOne #3761952). cron.php only runs +# application-level jobs that already execute as www-data under Apache, so it +# doesn't need root. +RUN echo "* * * * * www-data /usr/bin/php -f /var/www/simplerisk/cron/cron.php > /dev/null 2>&1" >> /etc/cron.d/simplerisk-cron && \\ chmod 0644 /etc/cron.d/simplerisk-cron RUN echo "0 0 * * * root /usr/sbin/logrotate /etc/logrotate.d/simplerisk.conf > /dev/null 2>&1" >> /etc/cron.d/logrotate-cron && \\ chmod 0644 /etc/cron.d/logrotate-cron