Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e9c22e3
fix(deps): update phpunit/phpunit to 13.4.5 for GHSA-9vw2-f4vp-x7gf
knittingcode Oct 28, 2026
c08a19b
fix(edge): build Caddy with go-jose v4.1.6
knittingcode Oct 28, 2026
3f2e70b
docs(security): triage CVE-2026-116587 in the edge Caddy binary
knittingcode Oct 28, 2026
2266471
docs(security): cite the scan runs of CVE-2026-116587 and link it fro…
knittingcode Oct 28, 2026
cf9dc3e
docs(security): rewrap the CVE-2026-116587 introduction
knittingcode Oct 28, 2026
762ea70
chore(merge): bring #40 (phpunit 13.4.5) into the branch
knittingcode Oct 28, 2026
2307a31
test(ci): guard the edge configuration against ACME server and mutabl…
knittingcode Oct 28, 2026
955449a
test(app): check the adapted edge configuration in the built images
knittingcode Oct 28, 2026
b6e78c9
docs(security): document the edge configuration guard
knittingcode Oct 28, 2026
102eda6
fix(ci): require exact single-package purls and reject expired scan e…
knittingcode Oct 28, 2026
c0a1d89
test(app): list suppressed findings in the container scan log
knittingcode Oct 28, 2026
c4e5bca
fix(security): except go-jose v3 in the edge binary until 2026-11-30 …
knittingcode Oct 28, 2026
9ab7bfb
docs(adr): propose ADR-021 for the temporary go-jose v3 exception
knittingcode Oct 28, 2026
820b100
fix(ci): check every file the edge can import and its edge service co…
knittingcode Oct 28, 2026
8fcd045
docs(security): state precisely what the edge guard covers and misses
knittingcode Oct 28, 2026
7230a3a
fix(ci): require paths and one entry per package and vulnerability in…
knittingcode Oct 28, 2026
5771129
test(app): drop the no-op --show-suppressed from the template scans
knittingcode Oct 28, 2026
5e5a251
docs(security): rewrap the exception policy and ADR-021 visibility text
knittingcode Oct 28, 2026
df50f91
fix(ci): resolve Compose anchors on bash 5.2 and close edge guard eva…
knittingcode Oct 28, 2026
e5fa201
docs(security): state the edge guard threat model and remaining evasions
knittingcode Oct 28, 2026
6364e82
fix(ci): compare exception packages without regard to case
knittingcode Oct 28, 2026
84c443d
docs(security): state the review-dependent limit of the alias check
knittingcode Oct 28, 2026
2d2a6c6
docs(security): list a renamed second Caddy binary as a known limit o…
knittingcode Oct 28, 2026
e82ac33
docs(adr): record the owner's acceptance of ADR-021
knittingcode Oct 28, 2026
3ae0292
fix(ci): reject YAML aliases, interpolation and parser directives tha…
knittingcode Oct 28, 2026
1dbeab6
docs(security): list the Compose and Dockerfile forms the edge guard …
knittingcode Oct 28, 2026
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,20 @@ jobs:
- name: Run container scan exception guard regression tests
run: scripts/ci/tests/guard-trivyignore.test.sh

- name: Keep the edge configuration free of ACME server, PKI, issuers and mutable admin
run: scripts/ci/guard-edge-config.sh

- name: Run edge configuration guard regression tests
run: scripts/ci/tests/guard-edge-config.test.sh

- name: ShellCheck
run: >-
docker run --rm --network none -v "$PWD:/mnt:ro" -w /mnt
koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d
scripts/ci/guard-tracked-files.sh scripts/ci/tests/guard-tracked-files.test.sh
scripts/ci/guard-dockerfile-frontend.sh scripts/ci/tests/guard-dockerfile-frontend.test.sh
scripts/ci/guard-trivyignore.sh scripts/ci/tests/guard-trivyignore.test.sh
scripts/ci/guard-edge-config.sh scripts/ci/tests/guard-edge-config.test.sh

- name: actionlint
run: >-
Expand Down
2 changes: 1 addition & 1 deletion backend/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"phpstan/phpstan-doctrine": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-symfony": "^2.1",
"phpunit/phpunit": "^13",
"phpunit/phpunit": "^13.4.5",
"symfony/browser-kit": "7.4.*",
"symfony/css-selector": "7.4.*"
}
Expand Down
16 changes: 8 additions & 8 deletions backend/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docker/edge/caddy/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.4 // indirect
github.com/go-chi/chi/v5 v5.3.2 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.10.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions docker/edge/caddy/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down
206 changes: 206 additions & 0 deletions docs/architecture/adr/ADR-021-edge-go-jose-v3-exception.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# ADR-021: Temporary exception for go-jose v3 in the edge Caddy binary

- **Status:** accepted by the owner on 2026-10-28 (FCRM-SEC-042M), Option A
with the conditions under "Acceptance". Proposed in FCRM-SEC-041 and
FCRM-SEC-042. The vulnerable code is not fixed; the decision accepts a
temporary risk.
- **Related:** [container vulnerabilities](../../security/container-vulnerabilities.md)
(exception policy, CVE-2026-116587 triage, edge configuration guard),
ADR-017 (edge routing)

## Context

Trivy reports CVE-2026-116587 / GHSA-mgcc-cww5-fp2q (HIGH, CVSS 7.5,
CWE-407/770) in `usr/bin/caddy` of the edge images. go-jose parses the RSA
modulus and primes of a JWK without bounds before `crypto/rsa` validation,
so a crafted key costs cubic CPU time: a denial of service, with no impact
on confidentiality or integrity.

| Module | Version | Fixed in | Handling |
|---|---|---|---|
| `github.com/go-jose/go-jose/v4` | 4.1.6 | 4.1.6 | upgraded (was 4.1.5); not excepted |
| `github.com/go-jose/go-jose/v3` | 3.0.5 | no fixed release (3.0.5 is the newest v3) | this decision |

go-jose v3 cannot be removed today:

- `modules/caddytls` (internal issuer, CA pools) and
`caddyconfig/httpcaddyfile` import `modules/caddypki`, and
`modules/caddyhttp` imports `caddytls`. Every Caddy build with the HTTP
app therefore links `caddypki` → `smallstep/certificates/authority` →
`go.step.sm/crypto/jose` → go-jose v3. A build from the
`modules/standard` list without `caddypki` still contains v3 3.0.5.
- The newest releases still require v3: Caddy 2.11.7,
`smallstep/certificates` 0.30.2 and `go.step.sm/crypto` 0.91.1. The move
of `smallstep/crypto` to go-jose v4 is open as smallstep/crypto#1157 and
needs a `smallstep/certificates` release on top.
- go-jose v3 receives only critical security updates, and no backport of
the fix exists.

Reachability in FlowCRM:

- **Caddy code:** only Caddy's `acme_server` handler passes untrusted input
to the vulnerable parser. It parses the JWS of an unauthenticated ACME
request before any check. `tls internal` and the `pki` app parse only
keys that Caddy creates or loads from its own configuration, and Caddy's
ACME client does not link go-jose.
- **FlowCRM's configuration:** no edge configuration uses `acme_server`, a
`pki` app, `tls internal`, `local_certs` or an issuer. Both Caddyfiles set
`admin off`, `auto_https off` and `persist_config off`, and the HTTPS test
edge loads a static certificate.
- **Runtime check:** the advisory's proof of concept (a JWK with a
16384-bit RSA prime) got a 404 from every HTTPS test edge port, using
about 0 s of CPU. With `acme_server` enabled, one unauthenticated request
cost about 2.2 s of CPU.

The runtime gate (`tests/app/scan.sh`) fails on every HIGH or CRITICAL
finding, so this finding blocks every pull request and `main`.

## Decision

1. Upgrade go-jose v4 to 4.1.6 (PR #41). v4 gets no exception.
2. Add one temporary exception to `security/trivyignore.yaml` that matches
only this finding:

```yaml
- id: CVE-2026-116587
paths: ["usr/bin/caddy"]
purls: ["pkg:golang/github.com/go-jose/go-jose/v3@v3.0.5"]
expired_at: 2026-11-30
```

Trivy applies an entry only when the id, path and purl all match. The
pinned Trivy 0.67.2 was run on real images to check this:
- the v4 4.1.5 finding still fails the gate;
- a v3 3.0.4 build still fails it;
- the control scan keeps its 28 HIGH and CRITICAL findings;
- api-prod and db suppress nothing;
- with an expiry in the past, the finding blocks again.
3. Compensating controls, which land in the same change as the exception:
- **Edge configuration guard**, `scripts/ci/guard-edge-config.sh` in the
`Repository guard` job. It parses every tracked Caddy file like Caddy
does, including imports, import globs and snippets, and every place
that starts Caddy. It rejects:
- `acme_server`, `pki`, issuers, `tls internal`, `local_certs` and
other ACME options, in every Caddy file and in every file copied or
mounted into `/etc/caddy`, whatever its name;
- a global block other than exactly `admin off`, `auto_https off` and
`persist_config off`;
- environment placeholders;
- `--resume` and `--watch`;
- other configuration files, and `CADDY_ADMIN`;
- edge service commands in Compose other than the exact `caddy run`
form, entrypoints, YAML escapes, and mounts over `/etc` or the
binary;
- edge Dockerfile stages that could link, copy, rename or shadow the
binary (RUN other than `apk add`, WORKDIR, relative or variable
COPY destinations, anything else in a bin directory, PATH);
- quoted keys, flow mappings and non-canonical paths in Compose
files, and long-syntax mounts over `/etc` or a bin directory.

Unknown Caddyfile constructs fail. Shell, YAML and Dockerfile text is
read line by line. Threat model: the guards stop accidental and casual
unsafe changes and make deliberate ones visible in review. A committer
can also change the guard itself, so they are not a boundary against
one (see Consequences).
- **Adapted-configuration check**, in `tests/app/scan.sh`. It runs
`caddy adapt` with the built binary in the built edge images and on
the HTTPS test edge, and checks the JSON: the admin API is disabled,
there is no `pki` app, no TLS automation or issuer and no
`acme_server` handler, automatic HTTPS is off and persistence is off.
An unsafe control configuration must be rejected.
- **Stricter exception guard**, `scripts/ci/guard-trivyignore.sh`. Every
entry must name its path and exactly one package with an exactly
pinned purl; no package may appear in two entries, and an id that the
exceptions table lists as an alias (the GHSA id of this CVE) is
rejected, because Trivy matches only the id it reports. The alias
check relies on the table, so a change to its first column is checked
in review. An entry that expires on or before the check date fails
the guard.
- **Visibility:** the scan template prints the suppressed finding as
`SUPPRESSED` in the edge-prod scan log. The edge `dev` report scan
does not use the ignore file and keeps listing it.
4. The exception is listed in the exceptions table of the container
vulnerability document, which states that it does not fix the vulnerable
code.

## Acceptance

Accepted by the owner on 2026-10-28 with the following scope and conditions:

- **Scope:** only CVE-2026-116587 in
`pkg:golang/github.com/go-jose/go-jose/v3@v3.0.5`, path `usr/bin/caddy`,
in the edge runtime image, until 2026-11-30. go-jose v4, other v3
versions, other packages, other binaries and every other HIGH or CRITICAL
finding stay blocking. The finding stays visible in the scan logs.
- **Conditions:** the compensating controls above stay in place and pass in
CI. The exception is reassessed immediately on any review trigger listed
under Consequences, and before it expires.
- **Removal:** when an exit criterion is met, delete the entry from
`security/trivyignore.yaml` and its row from the exceptions table, and
mark this ADR superseded. Renewing it after the expiry needs a new review
and a new owner decision. Without either, the exception guard and the
gate fail from 2026-11-30.

## Consequences

- **The vulnerable code stays in the binary.** The exception only stops the
gate from blocking on it.
- **Residual risk:** availability only. If a configuration enabled an ACME
server, or a mutable admin API, a remote client could exhaust the edge's
CPU. The edge has no CPU or memory limits, so the API and database on the
same host could suffer too. An ordinary change that enables such a
configuration fails the static guard. The adapted check also catches it
when the change is in the built image. The checks do not see commands or
paths assembled at run time (shell variables, `eval`, an `env_file` that
sets `CADDY_ADMIN` or a `{$VAR}` value, `docker cp`). They do not see
`docker run --entrypoint` with another executable in a script outside
Compose, a second Caddy binary copied under another name into a new
Dockerfile and started with its own configuration, a Caddy plugin added
to `docker/edge/caddy/main.go`, or what an `apk add` package installs. They do not recognise a service that runs an
already built edge image under a tag without "caddy". The adapted check
does not apply Compose mounts. A committer can also edit the guards. So
review of every change under `docker/edge`, `tests/app/https`, the Compose
files and `scripts/ci` remains required, and such a change is a review
trigger below.
- **Expiry:** on 2026-11-30 Trivy stops applying the entry, and the
exception guard fails the `Repository guard` job, so every pull request
is blocked until the exception is renewed after a new review or removed.
The maximum renewal under the policy is 90 days.
- **Review triggers.** The exception is reviewed early, and removed as soon
as it is no longer needed, on any of:
- a go-jose v3 release with the fix;
- a `go.step.sm/crypto` and `smallstep/certificates` release on go-jose
v4, or a Caddy release without v3;
- the further go-jose v3 advisories GHSA-4w3g-jfj2-cw2q and
GHSA-vvm8-gf78-28g4 reaching the advisory database;
- a Go vulnerability database entry with affected symbols, for a
govulncheck check;
- a severity change;
- any change under `docker/edge` or `tests/app/https`, to a Compose file,
to `scripts/ci`, or a new Caddy plugin;
- a Trivy upgrade;
- a deployment beyond the loopback-only development and test stacks.
- **Exit criteria:** go-jose v3 is gone from `go version -m usr/bin/caddy`,
or replaced by a fixed version. The exception entry and its table row are
then removed, and this ADR is superseded.
- **Operations:**
- the edge configuration guard takes about 2 s, and its regression
suite about 2.5 minutes, in the `Repository guard` job;
- the adapted check adds seconds to the scan step;
- a renewal is a reviewed change to `security/trivyignore.yaml` and the
exceptions table.
- **Rollback:** revert the exception commit. The gate then fails on the
finding again, and the guards stay in place.

## Alternatives considered

- **Wait for an upstream fix without an exception:** keeps the gate strict,
but blocks every merge, including unrelated security fixes, for an
unknown time.
- **A Caddy build without `caddypki`:** does not remove v3 (see Context).
- **A `replace` with a patched go-jose v3, or a Caddy fork:** a
self-maintained fork of a cryptographic library or of Caddy's TLS stack,
in the TLS edge. This is an architecture change, and it is not proposed.
- **An exception by id or by path only, a 90-day expiry, a report-only
edge-prod scan, or a lower threshold:** rejected. They would also hide
the fixable v4 finding, or weaken the runtime gate.
5 changes: 5 additions & 0 deletions docs/architecture/development-lifecycle-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ apply unchanged to FND-004. Read-only commands (`status`, `url`, `ports`,
reported ([container vulnerabilities](../security/container-vulnerabilities.md)).
Every scan must also inventory the expected package sources of the image,
so an unrecognised image cannot pass as "no findings".
- The edge configuration must not enable the ACME server, the PKI app, an
issuer or a mutable admin configuration: `scripts/ci/guard-edge-config.sh`
in `Repository guard` checks the sources, and `tests/app/scan.sh` checks
the configuration adapted by the built images
([container vulnerabilities](../security/container-vulnerabilities.md)).
- Image references, including the Dockerfile `# syntax=` frontend, stay
pinned by digest (the frontend is enforced by
`scripts/ci/guard-dockerfile-frontend.sh` in `Repository guard`); actions
Expand Down
9 changes: 7 additions & 2 deletions docs/dev-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,13 @@ Application suites (`tests/app/`), each in its own Compose projects:
development images, which are reported; each scan must inventory the
expected package sources (Alpine packages, Composer, the Caddy binary,
Node packages), and control scans prove the gate fails on a vulnerable
image and on an image without inventoried packages (see
[security/container-vulnerabilities.md](security/container-vulnerabilities.md)).
image and on an image without inventoried packages; it also adapts the
edge configurations (prod, dev, HTTPS test edge) with the built Caddy
binary and rejects admin, config persistence, PKI, issuers, ACME and the
`acme_server` handler, with a control image that must fail (see
[security/container-vulnerabilities.md](security/container-vulnerabilities.md),
"Edge configuration guard"; the static half is
`scripts/ci/guard-edge-config.sh` in `Repository guard`).
- `backend.sh` (BE-01 to BE-18): PHP 8.5 runtime, `composer validate` and
`composer audit`, PHP-CS-Fixer, PHPStan (level max), migrations from an
empty database, `doctrine:schema:validate`, a full down/up migration round
Expand Down
Loading