Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .grype.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 `<VERSION>-php83/-php84/-php85` (multi-arch `linux/amd64,linux/arm64`) and `simplerisk` gets `<VERSION>-jammy/-noble` (amd64). Each image's default variant also takes the bare `<VERSION>` 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 <PHP-version>`. Before assuming a curl CVE finding is real, check `grype -o json <image> | 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}`.
2 changes: 1 addition & 1 deletion simplerisk-minimal/.testing-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20260909-001
20260917-001
25 changes: 11 additions & 14 deletions simplerisk-minimal/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 <support@simplerisk.com>"

ENV version=20260909-001
ENV version=20260917-001

WORKDIR /var/www

Expand Down Expand Up @@ -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 && \
Expand Down
29 changes: 29 additions & 0 deletions simplerisk-minimal/common/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,33 @@ set_csrf_secret(){
[ -n "${SIMPLERISK_CSRF_SECRET:-}" ] && echo "<?php \$secret = \"${SIMPLERISK_CSRF_SECRET}\"; ?>" > "$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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ SSLStrictSNIVHostCheck Off
Options -Indexes
</Directory>
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
Expand Down
21 changes: 9 additions & 12 deletions simplerisk-minimal/generate_dockerfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \\
Expand Down
2 changes: 1 addition & 1 deletion simplerisk-minimal/stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 10 additions & 4 deletions simplerisk/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 <support@simplerisk.com>"
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion simplerisk/generate_dockerfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading