Skip to content

Security assurance

The threat model says what the server defends against and points at the code. This page says what checks the code without anyone remembering to, where you can read each result yourself, and, in plain words, what this project has not yet done. A security page that lists only the good news is hard to trust, so the last section is not optional.

Everything in this table runs from .github/workflows/security.yml on every push to main, on every pull request and every Monday. The weekly run matters more than it looks: a scan that only fires on a push goes quiet the moment the code does, and a new advisory against an unchanged dependency is, in our experience, the common case.

Check What it covers What happens on a finding Where to read it
CodeQL, extended security suite Every TypeScript and JavaScript file in the repository, the website and tests included, and the GitHub Actions workflows themselves, including the ones holding the deploy and registry credentials An alert in the repository’s code scanning list; a pull request is annotated inline Workflow runs, the Security tab
Dependency audit (bun audit) Both lockfiles: the server’s and the website’s, against the GitHub Advisory Database The run fails on any advisory of high or critical severity, which blocks the pull request The run log, which is public
Dependency review Every package and action a pull request adds The run fails on a high-severity advisory or a disallowed licence and comments on the pull request The pull request
Container image scan (Trivy) The image built from the repository’s own Dockerfile, the same way the release workflow builds it Critical and high findings with an available fix are uploaded to code scanning; unfixed ones are omitted because there is nothing to do about them yet The Security tab, category trivy-image

The released image, and how to check it is ours

Section titled “The released image, and how to check it is ours”

Everything above checks the code. This section is about the artifact you actually run. Every image release.yml publishes carries three things, and you can verify each of them yourself instead of taking our word for it:

What Produced by What it lets you establish
A signature cosign sign, keyless, over the image digest These exact bytes were pushed by this repository’s release workflow, running on a version tag, and not by anyone holding a token
An SBOM BuildKit (sbom: true), attached to the image as an attestation Every package inside the image, so you can answer “is today’s advisory in here?” without waiting for us to publish an answer
Build provenance BuildKit (mode=max) and a signed SLSA v1 statement through Sigstore Which commit, which base image digest, which Dockerfile and which build inputs produced it

The signing is keyless: the workflow exchanges its GitHub OIDC token for a short-lived Sigstore certificate that names the repository, the workflow file and the tag, and that certificate is what a verification checks. There is no long-lived signing key in the project, so there is none to leak or rotate — and no version of “trust this key we published somewhere”.

Verify the signature, naming the identity that is allowed to have produced it:

Terminal window
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/RedFox-Soft/OAuth-server\.ts/\.github/workflows/release\.yml@refs/tags/v' \
ghcr.io/redfox-soft/oauth-server-ts:<version>

The identity is what the command is for. cosign verify without those two flags asks only “is this signed by somebody”, which every attacker with a Sigstore account can also satisfy.

The release workflow runs that same command against the image it has just signed, with the identity pinned to its own tag, before the run is allowed to finish. So a signature that cannot be verified fails the release instead of reaching you. Nor can the command above quietly stop working here without going red there first.

Read the SBOM and the build record, both of which travel inside the image index:

Terminal window
docker buildx imagetools inspect ghcr.io/redfox-soft/oauth-server-ts:<version> \
--format '{{ json .SBOM }}'
docker buildx imagetools inspect ghcr.io/redfox-soft/oauth-server-ts:<version> \
--format '{{ json .Provenance }}'

And verify the signed SLSA provenance, which is filed in GitHub’s attestation store and recorded in a public transparency log:

Terminal window
gh attestation verify oci://ghcr.io/redfox-soft/oauth-server-ts:<version> \
--repo RedFox-Soft/OAuth-server.ts

The SBOM matters more for this image than for most, because the Dockerfile chooses to run apk upgrade on top of a digest-pinned base: the distribution package set is therefore a property of the day the image was built and cannot be recovered by reading the repository. The SBOM is the record of it, which is also why the same Dockerfile does not claim a reproducible build.

A release carries two files besides its notes: docs-export.json, the reference data these documentation pages are built from, and the CHANGELOG.md as it stood at the tag. Both are covered by a single signed SLSA v1 provenance statement, produced the same keyless way as the image’s and filed in the same public transparency log. It ships as a third asset, release-assets.intoto.jsonl, so you can check a download without reaching back to GitHub for the statement:

Terminal window
gh attestation verify docs-export.json \
--bundle release-assets.intoto.jsonl \
--repo RedFox-Soft/OAuth-server.ts \
--signer-workflow RedFox-Soft/OAuth-server.ts/.github/workflows/release.yml

Drop --bundle and the same command verifies against the attestation store instead. Either way the statement names the commit, the workflow and the run that produced the file, which is the question worth asking about a data file somebody else’s build reads.

The release workflow runs both verifications, one per asset, against the bundle it is about to attach, before the release is created.

.github/workflows/scorecard.yml runs the OpenSSF Scorecard weekly and on every push to main, and publishes the result to scorecard.dev. The checks are somebody else’s: branch protection, whether actions and dependencies are pinned, whether a fuzzer exists, whether releases are signed, how quickly vulnerabilities are addressed. A low score on a check is information, and nothing to hide; the report is linked from the README badge so that it is the first thing a reader sees.

Turned on in the repository’s settings, and visible to anyone who looks:

  • Secret scanning with push protection, so a committed credential is refused before it lands.
  • Dependabot alerts and security updates, and a Dependabot configuration that also keeps the routine versions of both lockfiles and every action current on a weekly cadence.
  • Private vulnerability reporting, so a researcher who prefers not to email can open a private advisory instead. Either path reaches the same people, with the response times the security policy commits to.

The controls in the threat model are held by tests, and the suite runs on every push and every pull request in ci.yml, with coverage published to Codecov. Several of those tests exist only to stop a control from quietly going away: one fails when a state-changing admin route is not in the audit table, one when an admin operation is neither published to the MCP tool list nor explicitly excluded with a reason, one when anything that looks like a secret reaches an audit entry or an MCP response. They are the reason the threat model can name a test in its evidence column and mean it.

Every deployment also serves a machine-readable security contact at /.well-known/security.txt (RFC 9116), pointing at the policy and the reporting address.

Stated plainly, because a reader deciding whether to run this in front of real users needs the whole picture.

  • No external audit. No third party has been paid to assess this code. When one is, the report is published on the security policy page in full, findings included.
  • No paid bug bounty. A confirmed report earns credit in the release notes and our thanks. There is no money in it. The scope and safe harbour in the security policy are the entire programme.
  • No OpenID Foundation certification. The suite has been run, and CONFORMANCE.md records the result — twelve plans, 16 680 conditions, and the defects still open. Certification itself has not been applied for, so compliance remains this project’s own claim.
  • No fuzzing, and no reproducible build. Scorecard will say so about both, and it is right about both. The build is not reproducible by design (see the apk upgrade above), and the Dockerfile argues that trade out in the open.

The list gets shorter as things are done, and never by deletion alone: an item leaves it by moving to one of the sections above with a link to the evidence.

Signed releases left this list that way. The release assets were unsigned through v0.2.0, on the argument that documentation nobody executes does not need a signature; every release after it carries provenance (above). The earlier ones cannot be given it retroactively, and Scorecard reads the last several releases, so its Signed-Releases check will keep reporting those two for as long as they are in range.