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
3 changes: 3 additions & 0 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ jobs:
- name: simplerisk-minimal DB TLS flags per mysql client
run: ./simplerisk-minimal/test_db_ssl_flags.sh

- name: simplerisk-minimal vendored RDS CA bundle
run: ./simplerisk-minimal/test_rds_bundle.sh

- name: simplerisk-minimal Apache worker setting
run: ./simplerisk-minimal/test_set_apache_workers.sh

Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/rds-bundle-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: RDS CA bundle drift

# The image ships AWS's RDS CA bundle (simplerisk-minimal/common/etc/ssl/rds/global-bundle.pem) so the setup client
# and the app can verify the database server's certificate. AWS publishes the bundle without a checksum, and adds
# certificates when it launches a region or replaces a root. This compares the vendored copy with the file AWS
# serves today once a week: a mismatch does not mean anything is broken (a bundle that lacks a NEW region's root
# only matters if we run RDS there), it means someone should refresh the file and its pin deliberately.
#
# Refresh: download https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem over the vendored file,
# update simplerisk-minimal/rds-global-bundle.sha256 (`sha256sum`), run simplerisk-minimal/test_rds_bundle.sh.

on:
schedule:
- cron: '0 14 * * 1' # Mondays 14:00 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
drift:
name: Compare the vendored RDS CA bundle with AWS's
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Compare
id: compare
run: |
set -euo pipefail
vendored=simplerisk-minimal/common/etc/ssl/rds/global-bundle.pem
curl -fsSL --retry 3 --max-time 60 -o /tmp/aws-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
grep -q 'BEGIN CERTIFICATE' /tmp/aws-bundle.pem || { echo "::error::AWS returned something that is not a certificate bundle"; exit 1; }
have=$(sha256sum "$vendored" | cut -d' ' -f1); now=$(sha256sum /tmp/aws-bundle.pem | cut -d' ' -f1)
subjects() { awk '/BEGIN CERTIFICATE/{n++} {print > ("/tmp/c" n ".pem")}' "$1"; for f in /tmp/c[0-9]*.pem; do openssl x509 -in "$f" -noout -subject -enddate | tr '\n' ' '; echo; rm -f "$f"; done | sort; }
if [ "$have" = "$now" ]; then
echo "vendored bundle is current ($(grep -c 'BEGIN CERTIFICATE' "$vendored") certificates, sha256 ${have:0:16}...)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
subjects "$vendored" > /tmp/have.txt; subjects /tmp/aws-bundle.pem > /tmp/now.txt
added=$(comm -13 /tmp/have.txt /tmp/now.txt | wc -l | tr -d ' '); removed=$(comm -23 /tmp/have.txt /tmp/now.txt | wc -l | tr -d ' ')
{
echo "### The vendored RDS CA bundle differs from AWS's"
echo "- vendored: sha256 \`${have:0:16}...\` ($(grep -c 'BEGIN CERTIFICATE' "$vendored") certificates)"
echo "- AWS now: sha256 \`${now:0:16}...\` ($(grep -c 'BEGIN CERTIFICATE' /tmp/aws-bundle.pem) certificates)"
echo "- certificates only in AWS's: $added; only in ours: $removed"
echo ""; echo "Certificates only in AWS's file:"; comm -13 /tmp/have.txt /tmp/now.txt | sed 's/^/- /' | cut -c1-200 | head -20
echo ""; echo "Refresh deliberately: see the header of .github/workflows/rds-bundle-drift.yml."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::The vendored RDS CA bundle differs from AWS's ($added certificate(s) only in AWS's, $removed only in ours). See the job summary."
exit 1

- name: Tell Slack
if: failure() && steps.compare.outcome == 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
[ -n "${SLACK_WEBHOOK_URL:-}" ] || exit 0
curl -fsS -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"The RDS CA bundle vendored in simplerisk/docker differs from AWS's. Refresh it deliberately: ${RUN_URL}\"}" \
"$SLACK_WEBHOOK_URL" >/dev/null || true
6 changes: 4 additions & 2 deletions simplerisk-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ docker run -d --name simplerisk -e SIMPLERISK_DB_PASSWORD=pass -e SIMPLERISK_DB_
| `DB_SETUP_PASS` | `root` (the bundled `stack.yml` ships `simplerisk_setup`) | Used when `DB_SETUP=automatic\|automatic-only\|delete`. Password of the privileged MySQL user used **only** to install the SimpleRisk schema and create the app DB user. In `stack.yml` it is also the bundled MySQL root password; since that MySQL is not exposed outside the stack network, a documented default is used for the zero-config trial. Override it (and `MYSQL_ROOT_PASSWORD` in `stack.yml`) for any non-trial deployment. |
| `DB_SETUP_WAIT` | 20 | Used when `DB_SETUP=automatic\|automatic-only`. Time, in seconds, the application is going to wait to set up the database. Useful if you are deploying the database and SimpleRisk at the same time |
| `DB_UPGRADE` | `null` (Accepts any value) | Run SimpleRisk's release-by-release database **schema upgrade** against an already-installed database (as opposed to `DB_SETUP`, which installs a fresh one), then continue running (`automatic`) or stop the container (`automatic-only`, for a one-shot upgrade job). Uses the `SIMPLERISK_DB_*` application credentials — no privileged user. The structured per-release JSON result is written to the container log, and the container exits non-zero if the upgrade fails. Idempotent — a no-op when the schema is already current |
| `DB_SSL_ENABLED` | `false` (off) | Opt-in, used when `DB_SETUP=automatic\|automatic-only\|delete`. Set to exactly `true` to require TLS on the privileged setup/delete MySQL client connection (adds `--ssl-mode=REQUIRED --enable-cleartext-plugin`; on the arm64 image, whose `mysql` is MariaDB's client and has no `--ssl-mode`, it adds the equivalent `--ssl --skip-ssl-verify-server-cert`). Any other value, or unset, leaves the connection unchanged (plaintext-capable) |
| `DB_SSL_ENABLED` | `false` (off) | Opt-in, used when `DB_SETUP=automatic\|automatic-only\|delete`. Set to exactly `true` to require TLS on the privileged setup/delete MySQL client connection. When the RDS CA bundle is present (`/etc/ssl/rds/global-bundle.pem`, shipped in the image) the server certificate is also **verified**, chain and host name: `--ssl-mode=VERIFY_IDENTITY --ssl-ca=<bundle> --enable-cleartext-plugin` with Oracle's client, `--ssl-ca=<bundle> --ssl-verify-server-cert` with MariaDB's (the arm64 image: Debian has no MySQL client and Oracle's apt repository has no arm64 packages). Without the bundle, or with `DB_SSL_VERIFY=false`, it encrypts without verifying (`--ssl-mode=REQUIRED --enable-cleartext-plugin`, or `--ssl --skip-ssl-verify-server-cert`). Any other value, or unset, leaves the connection unchanged (plaintext-capable) |
| `DB_SSL_VERIFY` | `true` | Only with `DB_SSL_ENABLED=true`. Set to exactly `false` to stop verifying the server certificate and only encrypt, without rebuilding the image (a rollback switch, not a setting to leave on) |
| `RDS_CA_BUNDLE` | `/etc/ssl/rds/global-bundle.pem` | CA file the setup/delete client verifies the server against. The image ships AWS's RDS global bundle there (114 certificates for every region, pinned by `rds-global-bundle.sha256`; see `.github/workflows/rds-bundle-drift.yml`) |
| `SIMPLERISK_DB_HOSTNAME` | `localhost` | Hostname of the database server |
| `SIMPLERISK_DB_PORT` | 3306 | Port to contact the database |
| `SIMPLERISK_DB_USERNAME` |`simplerisk` | User name to be used to access the SimpleRisk database |
| `SIMPLERISK_DB_PASSWORD` | `simplerisk` | Password to be used to access the SimpleRisk database. If not provided while setting up the database, a random password will be generated and shown on the container logs |
| `SIMPLERISK_DB_DATABASE` | `simplerisk` | Database name where all SimpleRisk objects are stored |
| `SIMPLERISK_DB_FOR_SESSIONS` | `true` | Indicator that the application will store all sessions on the configured database |
| `SIMPLERISK_DB_SSL_CERT_PATH` | Empty string (`''`) | Path where SSL certificates, to contact the database, are located |
| `SIMPLERISK_DB_SSL_CERT_PATH` | Empty string (`''`) | Path to the CA file the app uses to contact the database. **Empty means the app connects without TLS** (PDO is given no TLS option). Set it, for example to `/etc/ssl/rds/global-bundle.pem` for RDS, and PDO uses TLS and verifies the chain and the host name; calls the entrypoint makes with the app's own credentials (`MAIL_*` settings) use the same CA |
| `SIMPLERISK_CRON_SETUP` | `enabled` | Install the cron job to run in this Docker container |
| `SIMPLERISK_CSRF_SECRET` | Auto-generated | Override the auto-generated CSRF secret |
| `APACHE_MAX_REQUEST_WORKERS` | Apache default (150) | Maximum number of requests Apache serves at once (a whole number from 1 to 2000). Each request holds a database connection, so set this so that `replicas x APACHE_MAX_REQUEST_WORKERS` stays under the database's `max_connections`, leaving room for the cron jobs and for the second connection used when `SIMPLERISK_DB_FOR_SESSIONS` is `true`. Each worker is a full PHP process, so also keep it within the container's memory limit. It applies to the Apache prefork MPM the image ships with. An invalid value is ignored with a warning in the container log |
Expand Down
52 changes: 48 additions & 4 deletions simplerisk-minimal/common/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -384,13 +384,15 @@ apply_mail_setting(){
local db_key="$1" value="$2"
local escaped
escaped=$(sql_str_escape "$value")
# No --ssl-mode override: the client's default (PREFERRED) negotiates TLS when the
# server offers it. Forcing DISABLED made MySQL 8's caching_sha2_password refuse the
# first login ("Authentication requires secure connection"), silently skipping
# every mail setting that needs the app user's password.
# Never force a non-TLS connection: it made MySQL 8's caching_sha2_password refuse the first login
# ("Authentication requires secure connection"), silently skipping every mail setting that needs the app
# user's password. TLS flags come from app_db_tls_flags.
local -a tls=()
read -ra tls <<< "$(app_db_tls_flags)"
MYSQL_PWD="$SIMPLERISK_DB_PASSWORD" mysql -u "$SIMPLERISK_DB_USERNAME" \
-h "$SIMPLERISK_DB_HOSTNAME" \
-P "$SIMPLERISK_DB_PORT" \
${tls[@]+"${tls[@]}"} \
"$SIMPLERISK_DB_DATABASE" \
-e "UPDATE settings SET value='${escaped}' WHERE name='${db_key}';" \
|| print_log "mail_settings:warn" "Failed to update ${db_key}"
Expand Down Expand Up @@ -484,13 +486,55 @@ mysql_client_supports_ssl_mode() {
grep -q -- '--ssl-mode' <<< "$help"
}

# AWS's RDS CA bundle (all regions), vendored in this image from
# https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem and pinned by rds-global-bundle.sha256.
# It is what lets the mysql client and the app verify the database server's certificate.
RDS_CA_BUNDLE="${RDS_CA_BUNDLE:-/etc/ssl/rds/global-bundle.pem}"

# TLS flags that make the mysql client verify the server certificate (chain and host name) against the CA
# file $1, spelled for whichever client is installed.
mysql_verify_flags() {
if mysql_client_supports_ssl_mode; then
printf '%s' "--ssl-mode=VERIFY_IDENTITY --ssl-ca=$1"
else
# MariaDB's client: giving it a CA makes it verify the chain, --ssl-verify-server-cert adds the host name.
printf '%s' "--ssl-ca=$1 --ssl-verify-server-cert"
fi
}

# TLS flags for the mysql calls made with the app's own credentials (apply_mail_setting). They follow what the
# app's PHP connection does: with SIMPLERISK_DB_SSL_CERT_PATH set the app verifies against that CA, so this does
# too (and, like the app, does not fall back to plaintext if the file is missing: the call fails loudly).
app_db_tls_flags() {
if [ -n "${SIMPLERISK_DB_SSL_CERT_PATH:-}" ]; then
mysql_verify_flags "$SIMPLERISK_DB_SSL_CERT_PATH"
elif ! mysql_client_supports_ssl_mode; then
# MariaDB's client verifies by default and cannot chain RDS without a CA; Oracle's default does not
# verify. Keep both the same: encrypt if the server offers it, do not verify.
printf '%s' "--skip-ssl-verify-server-cert"
fi
}

set_db_ssl_flags(){
# Some databases require the privileged setup/delete mysql client to send
# its credential via the cleartext auth plugin, which the server only
# accepts over TLS (e.g. when DB_SETUP_PASS is a short-lived auth token
# rather than a static password). Set DB_SSL_ENABLED=true to opt in.
#
# With the RDS CA bundle present the connection is also VERIFIED (chain and host name). Set
# DB_SSL_VERIFY=false to go back to encrypt-without-verify without rebuilding the image; if the bundle is
# missing it falls back the same way, and says so.
DB_SSL_FLAGS=""
if [ "${DB_SSL_ENABLED:-}" = "true" ]; then
if [ "${DB_SSL_VERIFY:-true}" != "false" ] && [ -r "$RDS_CA_BUNDLE" ]; then
DB_SSL_FLAGS="$(mysql_verify_flags "$RDS_CA_BUNDLE")"
# The cleartext plugin is built into MariaDB's client; the option is obsolete there.
if mysql_client_supports_ssl_mode; then DB_SSL_FLAGS="$DB_SSL_FLAGS --enable-cleartext-plugin"; fi
return 0
fi
if [ "${DB_SSL_VERIFY:-true}" != "false" ]; then
print_log "db_tls:warn" "RDS CA bundle not readable at $RDS_CA_BUNDLE; connecting with TLS but NOT verifying the server certificate"
fi
if mysql_client_supports_ssl_mode; then
DB_SSL_FLAGS="--ssl-mode=REQUIRED --enable-cleartext-plugin"
else
Expand Down
Loading
Loading