kevgate

What it does
A small service that ranks scanner findings by CISA KEV listing, EPSS, CVSS and how critical the asset is. It ships with a container, a Helm chart, Terraform and a CI pipeline.
Why it matters
The point is to show what shipping a small service carefully looks like: authentication, rate limits, hardened containers, and CI that tests those controls.
What was found
Six defects had survived the 227 tests. Tests were added until each one fails when the defect is put back. The suite is now 277 tests.

See it Read the limits

Six defects had survived the test suite; tests added in 0.1.1 now fail when each one is reintroduced. The suite went from 227 to 277 tests.

As left: pass run 42960808156 passed 2026-11-11

  • survived the suite
  • killed by a test

6 survived 227 tests

6 killed by 277 tests

See it

Click a control to see the code and the CI checks behind it.

Plate 1

Recorded, not live. All 16 controls are listed here; with JavaScript on, a filter by layer and a detail panel replace this table.

16 controls of kevgate v0.1.1: the threat each addresses, where it is implemented, the tests and CI steps that cite it, and its limitation
ControlThreatImplemented atEvidenceLimitation
Application
API keys stored as hashes and compared in constant time

Timing side channel reveals key bytes or which key matched

Source: docs/THREAT_MODEL.md, line 46 (Spoofing)

Tests

CI steps

A leaked plaintext key stays valid until removed. There is no expiry, no last-used tracking, and revocation needs a restart

Source: docs/THREAT_MODEL.md, line 47

Application
401 for no or unknown key, 403 for a key without the scope

A read or write key performs admin actions

Source: docs/THREAT_MODEL.md, line 39 (Elevation of privilege)

Tests

CI steps

Scopes are global; there is no per-asset or per-team authorization

Source: docs/THREAT_MODEL.md, line 39

Ops endpoints are unauthenticated by design; rely on NetworkPolicy for /metrics

Source: docs/THREAT_MODEL.md, line 29

Application
Failed authentications throttled per source address before any key is checked

Brute-forcing a key or key id

Source: docs/THREAT_MODEL.md, line 37 (Spoofing)

Tests

CI steps

Per replica and in memory. Keyed by source address: clients behind one NAT or proxy share a budget, so an attacker there can lock legitimate clients out until the bucket refills (3 seconds per token at the default). A distributed attacker with many addresses is not slowed. The 256-bit key space is what actually defeats guessing; the limiter protects CPU, logs and alert noise

Source: docs/THREAT_MODEL.md, line 37

Application
Per-key token-bucket rate limit with Retry-After

One client floods the API

Source: docs/THREAT_MODEL.md, line 36 (Denial of service)

Tests

CI steps

Per replica and in memory. Queue and stats score every row on each call, which is fine at tens of thousands of findings but not millions

Source: docs/THREAT_MODEL.md, line 36

Application
Request body size limit on declared length and on streamed chunks

Large or slow bodies exhaust memory

Source: docs/THREAT_MODEL.md, line 35 (Denial of service)

Tests

CI steps

Slowloris-style slow uploads are left to the ingress and uvicorn timeouts

Source: docs/THREAT_MODEL.md, line 35

Application
Security response headers on every response

Browser or proxy caches findings

Source: docs/THREAT_MODEL.md, line 34 (Information disclosure)

Tests

CI steps

uvicorn's server header remains when launched with plain uvicorn and no --no-server-header

Source: docs/THREAT_MODEL.md, line 34

Application
JSON logs that carry a key id and never the key

Keys end up in logs

Source: docs/THREAT_MODEL.md, line 48 (Information disclosure)

Tests

CI steps

Upstream proxies may log the Authorization header if misconfigured

Source: docs/THREAT_MODEL.md, line 48

Application
Readiness checks storage headroom, a rolled-back write and database integrity

Disk fills or file is locked

Source: docs/THREAT_MODEL.md, line 59 (Denial of service)

Tests

CI steps

One writer at a time, and one SQLite file cannot be shared by several replicas. Horizontal scaling needs Postgres. A full disk is caught only when free space drops under the threshold, and there is still no volume-usage alert

Source: docs/THREAT_MODEL.md, line 59

Container
Non-root user, read-only root filesystem, all capabilities dropped

Container breakout from a root process

Source: docs/THREAT_MODEL.md, line 78 (Elevation of privilege)

CI steps

There is a shell in the image (Debian slim ships sh).

Source: docs/DEPLOYMENT.md, line 110

Cluster
NetworkPolicy: ingress from labelled pods only, egress to DNS only

Other pods scrape metrics or call the API

Source: docs/THREAT_MODEL.md, line 81 (Spoofing / disclosure)

CI steps

Ops endpoints are unauthenticated by design; rely on NetworkPolicy for /metrics

Source: docs/THREAT_MODEL.md, line 29

No WAF, ingress controller or TLS termination.

Source: docs/DEPLOYMENT.md, line 239

Cluster
Pod Security Admission enforces the restricted profile on the namespace

Container breakout from a root process

Source: docs/THREAT_MODEL.md, line 78 (Elevation of privilege)

CI steps

No admission policy engine (Kyverno or Gatekeeper) beyond Pod Security Admission.

Source: docs/DEPLOYMENT.md, line 241

Cluster
Prometheus alert rules for auth failures, forbidden use and rate limiting

invalid suggests key guessing or a revoked key still in use; forbidden suggests a valid key used outside its scopes.

Source: deploy/monitoring/kevgate-rules.yaml, line 97

Tests

CI steps

Alert rules and the dashboard never ran against a live Prometheus. They are checked by promtool unit tests and schema validation only.

Source: docs/DEPLOYMENT.md, line 53

Cluster
Sigma rules for Kubernetes audit events

Detects kubectl exec or attach into a kevgate pod. The kevgate image needs no interactive access in normal operation (no shell tooling, read-only root filesystem), so a session in the container is either break-glass debugging or an attacker with cluster credentials looking for the API key file or the SQLite database on /data.

Source: deploy/detections/k8s_exec_into_kevgate_pod.yml, line 5

CI steps

The Sigma rules never ran in a SIEM. audit-check.sh is a hand translation to jq that checks field names against a real kind audit log.

Source: docs/DEPLOYMENT.md, line 55

audit-check.sh checks 3 of the 4 rules against a real kind audit log; sigma check covers all 4.

Supply chain
Dependencies installed from a committed, hash-locked file

Vulnerable dependency

Source: docs/THREAT_MODEL.md, line 82 (Tampering)

CI steps

Dependabot does not recognise requirements.lock as an input, so the workflow is the check that covers it.

Source: docs/PIPELINE.md, line 80

Supply chain
Release image signed, with SBOM and provenance attestations

The aim is that a pull request cannot reach main with a known-vulnerable dependency, a leaked secret, a weakened pod spec or an unscanned image, and that a release carries proof of where it came from.

Source: docs/PIPELINE.md, line 3

CI steps

Registry check on 2026-11-11: ghcr.io/samiali38183/kevgate:0.1.1 resolves, with an anonymous token and no credentials, to sha256:526cae185004d9b7f2121d24cbe8e3783b8c4b814331dff96f11cea17c687d27. Attached to that digest: SBOM (CycloneDX), signature (cosign), provenance (SLSA). The SBOM lists 104 components; the SBOM and the provenance name that digest as their subject.

No image signature verification at admission. Signing exists; enforcing it in the cluster does not.

Source: docs/DEPLOYMENT.md, line 242

Verify the cosign signature at admission (Kyverno or a similar controller), deploy by image digest, and turn on branch protection with required checks for main.

Source: docs/DEPLOYMENT.md, line 274

Cloud
CI role trust policy pinned to one repository and one branch

The CI role's trust policy is the one place where a small edit (StringEquals to StringLike, or a * in the sub value) would let any workflow in the repository, including pull request runs, assume the role.

Source: docs/DEPLOYMENT.md, line 197

CI steps

The CI role's trust policy is pinned by a terraform test (see docs/DEPLOYMENT.md), but main has no branch protection, so whoever can push to main controls what may assume that role. The role has no consumer today.

Source: docs/THREAT_MODEL.md, line 91

It checks the policy as written in Terraform, not what AWS stores after an apply, and it does not cover branch protection on main: anyone who can push to main still controls what runs with that sub. Branch protection is not configured for this repository.

Source: docs/DEPLOYMENT.md, line 212

What this is. A map from each claim about a control to the code that implements it and to the checks that cite it. It does not evaluate anything in the browser, and it is not a test report: the results are the recorded state of the cited CI steps.

What evidence means here. A test or a CI step that asserts the behavior, and that passed in the run linked beside it. It is not an audit, and nothing was deployed to a real cloud account: the Kubernetes checks ran on a throwaway kind cluster, and the Terraform was never applied to AWS.

Every one of 16 controls cites at least one test or CI step; none of the cited tests or steps is evidence that the control is sufficient. Recorded, not live: every line number, test name, CI job and step comes from kevgate v0.1.1 (commit 973c8c1) and from real GitHub Actions runs, captured on 2026-11-11 with Python 3.12.2 and pytest 9.2.0: 34 cited tests (84 cases, all passed on a rerun) of 277 collected, and 24 CI step citations. The page evaluates nothing. Regenerate with python scripts/capture_kevgate_bench.py; python scripts/capture_kevgate_bench.py --check compares the stored data with a fresh capture and node scripts/test_bench_kevgate.mjs checks that the page shows exactly the stored data.
Checked against
Its own six surviving mutants and 16 controls; kind cluster in CI
Found, then left
As found Six mutants survived 227 tests
As left Six killed; 277 tests
Limits
Demonstration scope: one replica, SQLite, LocalStack only Full limits
Technical description and verification record: instrument, reference standard, date, how to reproduce (self-issued, not accredited)

A small vulnerability intake service. Scanner findings go in; a remediation queue comes out, ranked by CISA KEV membership, EPSS, CVSS and asset criticality. The repository also shows what shipping it carefully looks like: authentication, rate limits, container hardening, Kubernetes and Terraform, and a CI pipeline that tests the controls. This sheet is the v0.1.1 record, with a matrix that maps each control to the code and the checks that cite it.

Instrument
kevgate v0.1.1, commit 973c8c1 (annotated tag)
Reference standard
its own six surviving mutants and sixteen security controls, each cited to a test or a CI step (a kind cluster in CI for the cluster controls)
Measured on
2026-11-11, Python 3.12.2, fresh clone at tag v0.1.1; CI runs 2026-11-11
Issued by
The author. Self-issued verification record, not accredited.
Reproduce
git clone https://github.com/samiali38183/kevgate && cd kevgate && git checkout v0.1.1
pip install -e ".[dev]"
pytest --cov

As foundThe 0.1.0 tests would not have noticed six defects, and five gaps remained

Coverage numbers do not show whether a test would notice a defect being introduced. The review round after the first release (v0.1.0) asked of each control what would fail if it broke, and found six places where nothing would, plus five gaps in how the controls were observed or built.

The first finding is about the tests, not the code. v0.1.0 already compared keys with hmac.compare_digest and no early exit; the 227 tests would still have passed if someone had replaced it with ==. Each of the six findings below was checked again on 2026-11-11 against the v0.1.0 tag.

A mutant is a copy of the code with one deliberate defect. A test suite kills it if at least one test fails; a mutant that survives shows a check nobody was making.
Table, 6 rows: Reading ledger: what the review found in v0.1.0
Reading ledger: what the review found in v0.1.0
CheckExpectedObserved at v0.1.0VerdictEvidence
Six mutants against the suiteA failing test for eachAll 227 tests passed with each mutant applied, including mutants of the key comparison (an early exit, and == in place of compare_digest; the shipped code used compare_digest) and a body-size limit that counted only the last chunkAs found: failrerun script (below)
Alert after a restartThe first authentication failure after a restart is countedThe labelled kevgate_auth_failures_total series only came into existence on the first failure. The repository's comment added in 0.1.1 says Prometheus' increase() then has no earlier sample, so the first event would never alertAs found: failmetrics.py at v0.1.0
ReadinessNot ready when storage cannot take a write/readyz ran SELECT 1 and checked that the KEV catalog loaded. It did not look at free space, a failing write or database integrityAs found: failapp.py at v0.1.0
Failed authenticationRepeated bad keys from one source are throttledA bad or missing key was counted and refused with 401, with no limit per source address. The per-key bucket applied only after a key authenticatedAs found: failrequire() at v0.1.0
Dependency lockA committed lock with hashes, installed with --require-hashes by CIrequirements.lock was not in the repository. The security workflow generated it on each run with deploy/scripts/lock.sh, and ci installed with pip install -e ".[dev]", without hashesAs found: failci.yml at v0.1.0
OIDC trust policyWeakening the policy fails a checkNo test pinned it. The repository's own text says validate, tflint, checkov and Trivy accept a trust policy weakened to StringLike with a wildcard, so none of them would have caught itAs found: failDEPLOYMENT.md
Table, 6 rows: The six mutants, re-run on 2026-11-11 against both tags (one change at a time to the source, full suite, stop at the first failure)
The six mutants, re-run on 2026-11-11 against both tags (one change at a time to the source, full suite, stop at the first failure)
Mutant (a deliberate defect)v0.1.0, 227 testsv0.1.1, 277 tests
Early-exit key comparisonSurvivedKilled
== in place of hmac.compare_digestSurvivedKilled
BEGIN instead of BEGIN IMMEDIATESurvivedKilled
Body size counted from the last chunk onlySurvivedKilled
CSV merge taking the minimum CVSSSurvivedKilled
Upsert skipping EPSSSurvivedKilled

As leftSix mutants killed, five gaps closed, controls cited to tests and CI

Release 0.1.1 adds the tests that kill the six mutants (tests/test_mutation_kills.py, 227 tests become 277) and closes the other five findings. Each change below is in the repository's CHANGELOG and is cited by a test or a CI step in the matrix above.

Table, 6 rows: Reading ledger: what 0.1.1 changed
Reading ledger: what 0.1.1 changed
CheckExpectedObserved at v0.1.1VerdictEvidence
Six mutants against the suiteA failing test for eachEach mutant makes at least one of the 277 tests failAs left: passthe table above; run 42960808156 passed 2026-11-11
Alert after a restartThe first failure is countedAll three labelled series exist at 0 from startup (shown below)As left: passtest_labelled_counters_exist_at_zero_on_startup
ReadinessNot ready when storage cannot take a write/readyz also checks free space (1 MiB), a write taken inside a transaction that is rolled back, and a cached quick_check that is time-boxedAs left: passtests/test_readiness.py
Failed authenticationRepeated bad keys from one source are throttledA per-source failure limiter is checked before any key is compared. X-Forwarded-For is read only for peers in KEVGATE_TRUSTED_PROXIES (shown below)As left: passtests/test_auth_failures.py
Dependency lockA committed lock with hashes, installed with --require-hashesrequirements.lock is committed; ci, the image build and pip-audit use it. Dependabot is configured for pip, Actions and DockerAs left: passrun 42960808156 passed 2026-11-11 (ci), run 42960808037 passed 2026-11-11 (security)
OIDC trust policyWeakening the policy fails a checkA terraform test pins one Allow, two StringEquals conditions, aud and one exact sub for the repository and branch, with no wildcard. 4 test runs passAs left: passrun 42956666367 passed 2026-11-11 (terraform)

Run on 2026-11-11 from a fresh clone at tag v0.1.1 (commit 973c8c1), Python 3.12.2, on Windows 11, on localhost (port 8765, shown as 8000 below). Production mode with a generated key file, the rate limit set to 6 per minute per key and the failed-authentication limit set to 5 per minute (the default is 20). The data is synthetic (real CVE IDs, invented assets and scores) and the KEV catalog is the 5-entry sample bundled with the repo. Keys are shown as placeholders; they were throwaway keys for this run, and a search of the server log for all three found none.

$ curl -s localhost:8000/readyz
{"status":"ready","checks":{"db":"ok","storage":"ok","kev":"ok"}}

$ curl -s localhost:8000/metrics | grep -E "^kevgate_(auth|rate|kev)" | grep -v created
kevgate_auth_failures_total{reason="missing"} 0.0
kevgate_auth_failures_total{reason="invalid"} 0.0
kevgate_auth_failures_total{reason="forbidden"} 0.0
kevgate_rate_limited_total 0.0
kevgate_auth_throttled_total 0.0
kevgate_kev_entries 5.0
Output, 13 lines: curl against a local kevgate: auth, scopes, ingest
$ curl -si localhost:8000/queue            # no key
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="kevgate"
{"detail":"missing or malformed bearer token"}

$ curl -si localhost:8000/queue -H "Authorization: Bearer $SCANNER_KEY"   # findings:write only
HTTP/1.1 403 Forbidden
{"detail":"API key lacks required scope 'queue:read'"}

$ curl -si -X POST localhost:8000/findings -H "Authorization: Bearer $SCANNER_KEY" \
    -H "Content-Type: application/json" -d @seed.json
HTTP/1.1 201 Created
{"accepted":6}

The ranked queue, reformatted from the JSON response. The two highest-CVSS non-KEV findings (9.8) rank below a KEV finding with CVSS 8.8, which is the point of the scoring:

$ curl -s "localhost:8000/queue?limit=6" -H "Authorization: Bearer $ANALYST_KEY"
risk  kev    cve              asset       acr  cvss  epss
99.2  true   CVE-2024-3400    fw-edge-01    8  10.0  0.96
97.1  true   CVE-2021-34527   dc-01         8   8.8  0.97
96.5  true   CVE-2021-44228   web-01        7  10.0  0.97
91.8  true   CVE-2023-4966    vpn-02        6   9.4  0.94
37.1  false  CVE-2023-38545   build-03      3   9.8  0.50
34.4  false  CVE-2022-42889   app-07        4   9.8  0.20
Output, 25 lines: Rate limit, failed-authentication limit and metrics
$ (8 requests) curl -s -o /dev/null -w "%{http_code}" localhost:8000/stats -H "Authorization: Bearer $BURST_KEY"
request 1 -> 200  ...  request 6 -> 200
request 7 -> 429
request 8 -> 429

$ curl -si localhost:8000/stats -H "Authorization: Bearer $BURST_KEY"
HTTP/1.1 429 Too Many Requests
retry-after: 8
{"detail":"rate limit exceeded"}

$ (6 requests) curl -s -o /dev/null -w "%{http_code}" localhost:8000/queue -H "Authorization: Bearer kvg_wrong"
bad key 1 -> 401  ...  bad key 5 -> 401
bad key 6 -> 429

$ curl -si localhost:8000/queue -H "Authorization: Bearer $ANALYST_KEY"   # a valid key, same source
HTTP/1.1 429 Too Many Requests
retry-after: 11
{"detail":"too many failed authentication attempts"}

$ curl -s localhost:8000/metrics | grep -E "^kevgate_(auth|rate)" | grep -v created
kevgate_auth_failures_total{reason="missing"} 1.0
kevgate_auth_failures_total{reason="invalid"} 5.0
kevgate_auth_failures_total{reason="forbidden"} 1.0
kevgate_rate_limited_total 3.0
kevgate_auth_throttled_total 3.0

Trimmed: JSON bodies reflowed into a table; response headers other than the status line, www-authenticate and retry-after removed; the /metrics commands are simplified filters; the eight stats requests and the six bad-key requests were shell loops, written in short form here, and the bad-key run waited a minute after the earlier requests so the failure bucket was full. The valid-key request being refused is the documented trade-off of throttling by source address: a source that has spent its failure budget is refused before any key is compared.

Table, 9 rows: Results
Results
MeasureValueSource
Tests at the tag277 collected and 277 passed (pytest 9.2.0); 99.92% coverage with branches (1092 statements, 0 missed; 202 branches, 1 partly covered)pytest --cov from the fresh clone; run 42960808156 passed 2026-11-11 (ci, on both Python 3.11 and 3.12)
Lint and typesruff check and ruff format --check clean (ruff 0.17.4); mypy --strict src reports no issues in 16 source files (mypy 2.4.1)the same clone; run 42960808156 passed 2026-11-11
Mutants6 survive the 227 tests at v0.1.0; 6 are killed by the 277 tests at v0.1.1scripts/rerun_kevgate_mutants.py in this site's repository
CI on mainci, security and deploy-kind green on the tag commit; terraform green on the commit before it (the workflow only runs when infra/ changes, and nothing there changed since)run 42960808156 passed 2026-11-11, run 42960808037 passed 2026-11-11, run 42960807983 passed 2026-11-11, run 42956666367 passed 2026-11-11
Kind cluster12 smoke checks; labelled client allowed and unlabelled pod blocked; no outbound HTTPS from the pod; privileged pod rejected; 3 of the 4 Sigma-targeted audit events found in the API server logrun 42960807983 passed 2026-11-11 (deploy-kind)
ZAP baseline67 rules pass, 0 warnings, 0 failures, 1 rule ignored (listed with a reason in deploy/zap/rules.tsv)run 42960807983 passed 2026-11-11; PIPELINE.md
Terraform20 resources applied to LocalStack, empty second plan, 20 destroyed; 4 terraform test runs pass; checkov 0 failed (13 skipped, each justified inline)run 42956666367 passed 2026-11-11; DEPLOYMENT.md
Releasev0.1.1 (973c8c1, annotated tag, published 2026-11-11). v0.1.0 was the first release.GitHub release
Imageghcr.io/samiali38183/kevgate:0.1.1 is sha256:526cae18…. Signed with cosign (keyless) by the release job, which verified the signature before finishing. SBOM attestation lists 104 components; the SBOM and provenance statements name that digest as their subject. The package can be read without credentials: an anonymous token and manifest request both succeeded on 2026-11-11 (the same manifest request without a token gets 401, as a registry does). The tag has no v prefix on GHCR.run 42960813836 passed 2026-11-11 (release; v0.1.0: run 42922880036 passed 2026-11-10)

What was not done: the signature was verified by the release job, not again here (no cosign was available), and nothing in the cluster enforces it.

Technical details

How it works

design, implementation points and diagram

The problem

A raw scanner export sorts by CVSS, which puts a theoretical 9.8 ahead of a 7.5 that attackers are using today. KEV membership is the strongest prioritization signal that is freely available, so the service makes it the dominant factor and uses the rest to order everything else.

The second question is whether a small service like this can be shipped the way a production team would, with the security controls tested rather than described.

What it does

The score is 100 * (0.40*kev + 0.25*acr/8 + 0.20*epss + 0.20*cvss/10) / 1.05, from 0 to 100. The KEV weight equals the EPSS and CVSS weights combined, so a KEV finding never scores below a non-KEV finding on an asset with the same criticality; the one degenerate tie is broken in favor of KEV.

KEV is CISA's Known Exploited Vulnerabilities catalog: CVEs with evidence of exploitation in the wild. EPSS is a probability, from 0 to 1, that a CVE will be exploited in the next 30 days.

The API takes JSON findings or a Nessus/Tenable CSV export, upserts on (CVE, asset), and serves a ranked /queue, per-CVE views and stats. Scopes are findings:write, queue:read and admin. There are health, readiness and Prometheus endpoints, and a separate CLI command refreshes the KEV file from CISA so the API process never makes outbound calls.

Plate 2
kevgate request pathA client pod labelled kevgate-client=true reaches the Service on port 8000 only through the NetworkPolicy. The pod runs as UID 10001 with a read-only root filesystem and keeps its SQLite database on a volume mounted at /data.Request path, namespace kevgatePod Security restricted enforcedClient podlabel kevgate-client=trueANetworkPolicy kevgateingress: labelled pods, TCP 8000;egress: DNS onlyBService kevgateClusterIP, port 8000CPod kevgateUID 10001, read-only root, nocapabilities, no service-accounttokenDVolume kevgate-data/data: SQLite; keys from aread-only SecretEkevgate delivery pipelineThe ci, security, deploy-kind and terraform workflows run on pull requests and pushes to main. The release workflow runs on a version tag.Pipeline, .github/workflowspull requests and pushes to main, thentagsciruff, mypy, pytest (Python 3.11,3.12)FsecuritySemgrep, Bandit, pip-audit,gitleaks, checkov, TrivyGdeploy-kindkind and Calico: smoke,NetworkPolicy, Pod Security, ZAPHterraforminfra/ changes: validate, OIDCtest, LocalStackIrelease, on a version tagscan, push to GHCR, sign, SBOM,provenance, verifyJ
How the chart and the workflows at the tag fit together. Letters A to E follow a request; F to J are the workflows. The release workflow's attestations and the kind job's NetworkPolicy and Pod Security checks are among the evidence cited in Plate 1.
Describe this diagram

Request path, namespace kevgate. Pod Security restricted enforced.

  1. Client pod. label kevgate-client=true.
  2. NetworkPolicy kevgate. ingress: labelled pods, TCP 8000; egress: DNS only.
  3. Service kevgate. ClusterIP, port 8000.
  4. Pod kevgate. UID 10001, read-only root, no capabilities, no service-account token.
  5. Volume kevgate-data. /data: SQLite; keys from a read-only Secret.

Pipeline, .github/workflows. pull requests and pushes to main, then tags.

  1. ci. ruff, mypy, pytest (Python 3.11, 3.12).
  2. security. Semgrep, Bandit, pip-audit, gitleaks, checkov, Trivy.
  3. deploy-kind. kind and Calico: smoke, NetworkPolicy, Pod Security, ZAP.
  4. terraform. infra/ changes: validate, OIDC test, LocalStack.
  5. release, on a version tag. scan, push to GHCR, sign, SBOM, provenance, verify.

Three implementation points

Key checks that do not leak timing

Keys are 256-bit random values and the server stores only their SHA-256. KeyStore.authenticate hashes the presented token and compares it against every stored digest with hmac.compare_digest, never exiting early, so response time does not reveal whether a key matched or how many bytes were right. A fast hash is deliberate: slow password hashes exist to protect low-entropy passwords, and running one on every request would hand an attacker a CPU lever. Until 0.1.1 no test would have noticed if that loop were changed; the two tests that now do are in the matrix.

Limiters in the right order

A token bucket per API key returns 429 with Retry-After. A second limiter counts failed authentications per source address and is checked before any key is looked at, so a brute-force attempt is throttled instead of getting free guesses. The failure limiter uses peek and charge so that checking does not itself spend a token. Both are in memory, which is a stated limitation: they are per replica and reset on restart. Clients behind one NAT or proxy share a budget, so an attacker there can lock legitimate clients out until the bucket refills.

Controls proven by tests, and a hash-locked build

The kind job does not just render the Helm chart. deploy/scripts/netpol-test.sh starts a labeled pod that must reach the service, an unlabeled pod in the same namespace that must time out, checks that the kevgate pod cannot open an outbound HTTPS connection, and submits a privileged pod that Pod Security restricted must reject. A second script then confirms that three of the four audit events the repository's Sigma rules target are in the API server audit log. Dependencies are pinned in requirements.lock with hashes; CI and the Docker build install it with --require-hashes and pip-audit scans that exact file. A weekly lock-freshness workflow is meant to fail when a fresh compile would change the lock; it had no run on record when this page was checked.

Tolerance and limitsWhat the numbers do not show

  • Nothing is deployed to a real AWS account. The Terraform (ECR, reports bucket, KMS key, workload and CI roles) was applied only to LocalStack, which has no ECR in its Community edition, so the ECR repository and the GitHub OIDC role are checked statically and by the terraform test. Nothing uses those resources: the service has no AWS SDK code and releases go to GHCR.
  • One replica, SQLite on one volume, Recreate rollouts. That is a demonstration scope; DEPLOYMENT.md lists what production would change (Postgres, replicas, tested backup and restore, key lifecycle). The rate limiter and the failure limiter are per process, per source address, and reset on restart.
  • /metrics is unauthenticated. It carries counts, not findings, and is protected only by the NetworkPolicy.
  • No branch protection on main. The OIDC trust policy is pinned to refs/heads/main, so whoever can push to main controls what may assume that role. The role has no consumer today. The GitHub API returned "Branch not protected" for the repository on 2026-11-11.
  • The detection rules have not been run against a real SIEM or a live Prometheus. The Sigma rules convert with sigma and are validated; three of four are checked by a jq translation against a real kind audit log. The alert rules are checked with promtool unit tests only.
  • No image signature enforcement. The release is signed and verified by its own job, but nothing at admission checks the signature, and there is no admission policy engine beyond Pod Security Admission.
  • The matrix maps claims to evidence; it does not show sufficiency. A cited test passing shows that the behavior it asserts held on the tag, not that the control covers the threat. The repository's threat model lists the residual risks, and the matrix quotes them.
  • The scoring weights are a design choice, not evaluated against remediation outcomes. Access control is coarse: three scopes, no per-asset authorization, no key expiry or rotation workflow, and revocation needs a restart.
  • Bundled KEV file is a 5-entry sample. kevgate kev-refresh downloads the real catalog; the demo above used the sample.
Evidence

Where each number comes from

9 links

Chips show the state of each GitHub Actions run as captured when this page was built. The page does not call GitHub at runtime.

Security scans (security)
run 42960808037 passed 2026-11-11
Kind cluster controls (deploy-kind)
run 42960807983 passed 2026-11-11
Terraform (terraform, last run on main)
run 42956666367 passed 2026-11-11

CI evidence

The latest runs on main are the evidence for the controls and the matrix. Three workflows ran green on the tag commit (973c8c1); terraform has a path filter and last ran on the commit before it (b08a920), which has the same infra/ and workflow files. The release workflow ran on the tag. The results column is from the repository's own docs, which summarize those runs, and from the run logs.

Workflows and what each checks
WorkflowWhat it checksRun
ciruff, mypy, pytest with coverage, on Python 3.11 and 3.12; dependencies from the hashed lockrun 42960808156 passed 2026-11-11
securitySemgrep and Bandit (no findings), pip-audit on the lock, gitleaks, checkov (0 failed), Trivy on the image (no fixable HIGH or CRITICAL), Sigma and promtool checks, container smoke testrun 42960808037 passed 2026-11-11
deploy-kindkind with Calico: 12 smoke checks, NetworkPolicy ingress and egress, Pod Security restricted rejects a privileged pod, Sigma-targeted audit events present, restricted profile on the live pod, ZAP baselinerun 42960807983 passed 2026-11-11
terraformvalidate, tflint, checkov, terraform test on the OIDC trust policy, apply and destroy against LocalStackrun 42956666367 passed 2026-11-11
releaseBuild, scan, push to GHCR, cosign keyless signature, SBOM and provenance attestations, signature verified in the same runrun 42960813836 passed 2026-11-11