---
title: "Security assurance | FoxAuth"
description: "What backs the security claims, from the scanning that runs on every push and every week to where each result is published, and what has not been done yet."
canonical: "https://foxauth.dev/docs/security/assurance/"
lastmod: "2026-09-14T17:04:47+03:00"
---
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.

Continuous checksSection titled “Continuous checks”

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

Why the image scan does not fail the build

Almost everything an image scan finds is in the base image, and the remedy is
a rebuild once upstream ships a fix; there is nothing to change in this
repository. A red build that nobody can turn green teaches people to ignore
red builds. The finding is published instead, dated, where it stays visible
until the rebuild clears it.

The released image, and how to check it is oursSection 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.

What a signature does not say

It says where the image came from, and nothing about whether the code inside
is any good. A signed image with a critical vulnerability is signed and
vulnerable. Verification tells you that the artifact is the one this project
published for that version, so that the scanning, the threat model and the
tests on this page are evidence about the thing you are running rather than
about a lookalike.

The release’s own assetsSection titled “The release’s own assets”

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.

OpenSSF ScorecardSection titled “OpenSSF Scorecard”

.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.

Repository protectionsSection titled “Repository protections”

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 test suiteSection titled “The test suite”

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.

What has not been doneSection titled “What has not been done”

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.

PreviousThreat model for OAuth-server.tsNextSettings
