devresponsekit is a security-first authentication and multi-tenant application shell, so we take vulnerability reports seriously and aim to respond quickly.
Security fixes land on the main branch, which production deploys from, and
reach a tagged release through the next one. Releases are git tags
(vX.Y.Z) with a GitHub release each; v2.0.0 (2026-09-30) is the first,
since 1.0.0 was never tagged. CHANGELOG.md lists every change
per release, and anything merged since the latest one under Unreleased.
Report against the release or the commit SHA you tested.
| Version | Supported |
|---|---|
main (latest) |
✅ |
2.0.x (latest release) |
✅ |
1.0.0 (never tagged) |
❌ |
Please do not open a public issue, PR, or discussion for security vulnerabilities — that discloses the problem before a fix is available.
Instead, report privately via GitHub Security Advisories:
(If private reporting is not enabled on the repository, ask a maintainer to enable Settings → Security → Private vulnerability reporting.)
Please include, as far as you can:
- affected version / commit SHA,
- a description of the issue and its impact,
- reproduction steps or a proof of concept,
- any suggested remediation.
- Acknowledgement: we aim to confirm receipt within 5 business days.
- Assessment: we triage the report, confirm severity, and agree a fix timeline with you.
- Fix & disclosure: we develop the fix privately, release it, and then publish a GitHub Security Advisory crediting the reporter (unless you prefer to remain anonymous). We support coordinated disclosure and will agree a public-disclosure date with you.
Reports that bear directly on the security posture are highest priority, e.g.:
- authentication or session bypass (Better Auth integration, the
requireSecureSessionboundary), - cross-tenant isolation breaks (ADR-0001 — an actor reading or mutating data outside their organization),
- privilege escalation (RBAC / the
admin.*permission model, group/role conferral, impersonation), - machine-credential issues (API keys, JWT/JWKS, OAuth client credentials, scope intersection),
- SSO handoff abuse (nonce replay, audience confusion),
- injection, SSRF, XSS (including the Markdown docs viewer), or secret leakage.
Out of scope: findings that require a compromised host or database role, denial-of-service via unrealistic load, and issues solely in third-party dependencies (report those upstream — though we welcome a heads-up).
Two settings narrow the window in which a hijacked publish could reach a build (source review 2026-09-04, #226):
| Control | Where | What it does |
|---|---|---|
| Release cooldown | .npmrc → minimum-release-age=1440 |
pnpm refuses to resolve any version published less than 24 h ago. A hijacked publish is typically detected and yanked well inside that window. |
| Cooldown for proposals | .github/dependabot.yml → cooldown.default-days: 1 |
Dependabot does not open npm version-update PRs for releases younger than a day, so it never proposes a bump the .npmrc floor would refuse to re-resolve. Security updates are exempt from the cooldown, but Dependabot opens them only while the repository's Dependabot security updates setting is on, and no file can turn that on. See Repository security settings. |
| Package-manager integrity | package.json → packageManager: pnpm@<version>+sha512.<hash> |
Corepack verifies the pnpm tarball against this hash before running it (corepack use pnpm@<version> regenerates the pair). A tampered pnpm release fails the check instead of executing in CI and in the Docker build. pnpm/action-setup ignores the +sha512… suffix and installs the pinned version. |
The cooldown applies only where pnpm resolves a version. Every automated
install in this repo — CI, the Dockerfile, the deploy path — runs
pnpm install --frozen-lockfile, which resolves nothing and is therefore
never delayed, including on a Dependabot PR that pins a release published
minutes earlier. It bites only when a human adds or updates a dependency; to
take an urgent security release inside the window, override it for that one
command:
pnpm install --config.minimum-release-age=0 # document why in the PRThe Dependency audit workflow
(.github/workflows/dependency-audit.yml,
a required status check on main) fails the build on any high or critical
advisory (pnpm audit --audit-level high — a BUILD-1 hard gate) in either
lockfile: the app's, and the deploy CLI's in vercel-cli/
(pnpm --dir vercel-cli audit --audit-level high). The CLI is its own pnpm
package, and it handles VERCEL_TOKEN and the production direct database URL;
until F-28 no check audited it, and its advisories piled up as alerts nobody
read. Its override floors are documented in
vercel-cli/README.md. The workflow also
runs weekly (Mondays 05:13 UTC) so an idle main is re-audited as new
advisories are published. The weekly run also lists GitHub's open
high/critical Dependabot alerts (the Dependabot alerts job, which is not
a required check). GitHub's advisory database reports advisories the npm audit
endpoint misses, such as path-to-regexp@6 GHSA-9wv6-86v2-598j in
vercel-cli/. A failed scheduled run opens — or comments on — a GitHub issue
titled "Dependency audit failing on main" that names the failing tree,
and marks NOT AUDITED any lockfile whose audit a setup or install failure
skipped (review #227: the gate had been red for weeks with no commit to surface
it). A
Dependabot alert for an advisory you accept below must be dismissed in the
Security tab, citing its row, or the weekly job stays red.
A small, explicit allowlist mutes advisories that cannot be reached in what
ships. Each lockfile has its own, because pnpm audit reads the
pnpm.auditConfig.ignoreGhsas of the package it audits and no other:
- the app's lockfile (
pnpm-lock.yaml):package.json→pnpm.auditConfig.ignoreGhsas; - the deploy CLI's lockfile (
vercel-cli/pnpm-lock.yaml):vercel-cli/package.json→pnpm.auditConfig.ignoreGhsas. A GHSA added to the root list does not mute it in the CLI's audit.
Every entry in either list has a row below that names the lockfile it mutes
and carries a review-by date. When an upstream fix lands, drop the entry
rather than let it linger. tests/unit/dependency-governance.test.ts fails
when any lockfile's list mutes a GHSA that has no row.
| GHSA | Lockfile | Package | Severity | Why it is not reachable | Review by |
|---|---|---|---|---|---|
| none | — | — | — | Both allowlists are empty: the app's since 2026-09-04, and the CLI's has never had an entry. Every advisory is fixed by a version bump or an override floor (next section, and the CLI's floors). Add a row here, and the id to that lockfile's ignoreGhsas, only for an advisory that has no fixed release and meets the reachability rule below — never for anything reachable at runtime. |
— |
To re-verify reachability for a future entry:
- App lockfile.
pnpm why <pkg>must show it arriving only via dev/build/test tooling, and the Next.js standalone trace (output: "standalone") must exclude it from the runtime image. - Deploy CLI lockfile. The CLI never enters the runtime image, but it runs
with
VERCEL_TOKENand the production direct database URL, so "deploy-time only" is not a reason by itself. Try a floor invercel-cli/package.json→pnpm.overridesfirst. Mute only when no fixed release exists andpnpm --dir vercel-cli why <pkg>shows the package arriving only through codedrk-deploynever executes. The@vercel/nodeframework adapters undervercelare the model case: a Next.js deployment installs them and never runs them.
A new high/critical advisory that is not in these lists fails CI by design, so the gate still catches anything unreviewed.
The preferred fix for a vulnerable transitive is to raise its floor with
pnpm.overrides, not to mute the advisory: a mute's rationale rots the moment
a later CVE moves the patched line, while a floor keeps resolving to the fixed
release. Every override in package.json is listed here with the reason it
exists; tests/unit/dependency-governance.test.ts fails when an override is
added without a row (and when the lockfile resolves below the patched lines
the 2026-09 sweep established). Floors are scoped — to a parent
(parent>child) or a major (pkg@N) — so a floor can never cross a major
version behind a consumer's back. A floor that is not parent-scoped (pkg,
or pkg@N when the declared range is in major N) also rewrites the specifier
of a direct dependency of the same name, so a floor below the version
package.json declares silently wins over the declaration (after #481 a
^3.4.13 floor held the declared dompurify ^3.4.16 at 3.4.15). Set such a
floor at the declared version. Dependabot bumps only the direct entry, never
the floor, so the floor can fall behind again: the same test fails when a
lockfile resolves a direct dependency below its declared version, and the fix
is to raise the floor to the declaration. Review each row when its parent
ships a release that satisfies the floor on its own; the override can then go.
| Override | Floor | Why (advisories closed) | Scope / consumer | Review by |
|---|---|---|---|---|
jsdom>undici |
^8.9.0 |
GHSA-4cwx-7wf7-3272 (high). jsdom@30 declares undici@^8.9.0 (its network stack needs undici 8 — forcing 7.x hangs XHR/fromURL); the floor is the first patched 8.x release and dedupes with the direct dev pin. |
Dev (jsdom test environment). The direct dev undici is pinned 8.10.2 separately. |
2026-12-01 |
dompurify |
^3.4.16 |
GHSA-cmwh-pvxp-8882, GHSA-55q2-fjhq-7xh7 (moderate), GHSA-c2j3-45gr-mqc4 (low). Raised to the direct dependency's declared ^3.4.16: at ^3.4.13 it held the lockfile at 3.4.15 after #481 bumped the declaration. |
Runtime (mermaid on the in-app docs renderer, also a direct dependency). |
2026-12-01 |
postcss |
^8.5.28 |
GHSA-r28c-9q8g-f849 (high, sourceMappingURL path traversal), GHSA-fxqj-rqcc-2cmp (moderate); pulls nanoid@^3.3.18 (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8, high). next pins postcss@8.4.31. Raised to the direct dev pin 8.5.28. |
Build (Next.js + Tailwind), validated by pnpm build. |
2026-12-01 |
@babel/core |
^7.29.6 |
Dependabot alert #4. | Dev (Stryker instrumenter). | 2026-12-01 |
esbuild |
^0.28.1 |
Dependabot alert #3; vite declares ^0.27.0. |
Dev (vitest), validated by pnpm test:coverage. |
2026-12-01 |
next>sharp |
^0.35.0 |
GHSA-f88m-g3jw-g9cj (high — libvips CVE-2026-33327/33328/35590/35591). next@16.2.x declares sharp@^0.34.5 as an optional dependency. |
Runtime (next/image optimisation in the standalone server). Validated by pnpm build + the Trivy image scan. |
2026-12-01 |
js-yaml@3 |
^3.15.2 |
GHSA-52cp-r559-cp3m, GHSA-5p4m-2wfm-xmqj (high — merge-key / !!omap quadratic CPU), GHSA-h67p-54hq-rp68 (moderate). gray-matter declares ^3.13.1, which 3.15.x satisfies. |
Runtime (gray-matter docs frontmatter — repo-authored input only). Pinned by tests/unit/docs-frontmatter.test.ts. |
2026-12-01 |
js-yaml@4 |
^4.3.1 |
Same three advisories on the 4.x line. | Dev (@eslint/eslintrc, cosmiconfig via kysely-codegen). |
2026-12-01 |
ajv>fast-uri |
^3.1.8 |
GHSA-v2hh-gcrm-f6hx, GHSA-7p8r-x3mc-p8w7, GHSA-5jgf-p345-68v8, GHSA-f65p-4m7j-42xc, GHSA-fph4-wmhf-6fwf, GHSA-jqff-g426-hqxp (high); GHSA-hrr3-gc8f-f4qj (moderate, raised from ^3.1.6 on 2026-09-30). |
Dev/build (ajv under Stryker and webpack's schema-utils). |
2026-12-01 |
browserslist |
^4.28.7 |
GHSA-c83g-rgw3-j3cx, GHSA-73wf-gq98-2v4g (high). |
Build/dev (@babel/helper-compilation-targets, webpack via @sentry/bundler-plugins). |
2026-12-01 |
brace-expansion@1 |
^1.1.21 |
GHSA-3jxr-9vmj-r5cp, GHSA-mh99-v99m-4gvg, GHSA-rgw5-rvv9-x895 (high, ReDoS); GHSA-6j4f-fj2g-mc7p, GHSA-qhr7-859c-m2p7 (high, recursion stack exhaustion) and GHSA-q2hr-2g5m-vwhr (moderate), raised from ^1.1.18 on 2026-09-30. |
Dev (minimatch@3 under eslint). |
2026-12-01 |
brace-expansion@5 |
^5.0.12 |
The same six advisories on the 5.x line (raised from ^5.0.9 on 2026-09-30). |
Dev (minimatch@10 under Stryker and @openapitools/openapi-generator-cli's glob). |
2026-12-01 |
typed-rest-client>qs |
^6.16.0 |
GHSA-q8mj-m7cp-5q26, GHSA-x5fp-wj9c-mxmx, GHSA-4mjr-xmp4-gh2g (moderate). typed-rest-client pins qs@6.15.1 exactly. |
Dev (Stryker dashboard client). The direct dev qs is ^6.16.0. |
2026-12-01 |
socks>ip-address |
^10.7.1 |
GHSA-j6r3-76f7-8jcv, GHSA-h3mg-xc3c-68pw (moderate). |
Dev (@openapitools/openapi-generator-cli → proxy-agent → socks-proxy-agent → socks, which declares ^10.1.1). |
2026-12-01 |
lodash-es@4 |
^4.18.0 |
GHSA-r5fr-rjxr-66jc (high — _.template code injection). Arrived with mermaid@12, which adds chevrotain@11.1.2. Major-scoped, not parent-scoped, on purpose: chevrotain, @chevrotain/gast and @chevrotain/cst-dts-gen each declare "lodash-es": "4.17.23" — an exact pin at the top of the vulnerable range, so it can never float to the fix — and a chevrotain>lodash-es floor would leave the other two parents live. |
Runtime (mermaid → chevrotain, which mermaid 12 uses for the usecase diagram parser only; dagre-d3-es already resolved 4.18.1). Verified in Chromium: the docs diagrams and chevrotain-parsed usecase diagrams render identically with 4.17.23 and 4.18.1. |
2026-12-01 |
To confirm a floor took effect: pnpm why <pkg> must show a single resolved
version at or above the floor for every parent the row names, and
pnpm audit --audit-level low must report nothing for it (it reported 0
advisories at every level after the 2026-09-04 sweep).
-
2026-09 dependency sweep (review #8, #9, #26, #114). Both required supply-chain gates (
Dependency auditandtrivy) had gone red onmainwith 28 high advisories acrossnext@16.2.10(4 GHSAs incl.GHSA-6gpp-xcg3-4w24proxy bypass andGHSA-m99w-x7hq-7vfjServer-Actions DoS),next>sharp@0.34.5,undici,postcss/nanoid,fast-uri,browserslist,brace-expansion, andjs-yaml. Fixed by bumpingnext+eslint-config-nextto 16.2.12, the direct devundicito 8.10.2 andpostcssto 8.5.28, and by raising/adding the override floors in the table above. No advisory was muted. Validated withpnpm build, the fullpnpm test:coverage(ratchet intact), andpnpm audit --audit-level low(clean). -
js-yaml(GHSA-h67p-54hq-rp68, previously dismissed as "only v4 patches it"): that rationale went stale when js-yaml 3.15.x was published for the 3.x line. Thejs-yaml@3: ^3.15.2floor now keepsgray-matter's copy on the patched line (it satisfies gray-matter's^3.13.1), so the docs viewer is unchanged and no longer carries the two newer high advisories either. -
vitest(GHSA-5xrq-8626-4rwp, high — test runner only) was the soleignoreGhsasmute.vitest@4.1.9is no longer reported bypnpm audit(verified with the allowlist emptied on 2026-09-04), so the entry was dropped and the allowlist is empty. -
dompurify(GHSA-cmwh-pvxp-8882, moderate —ALLOWED_ATTRpollution viasetConfig) reached the runtime viamermaidon the in-app docs renderer. Pinned forward to the patched line withpnpm.overrides(nowdompurify: ^3.4.16);pnpm why dompurifyconfirms a single resolved version, and the mermaid render path stays defended bysecurityLevel: "strict"+ server-siderehypeSanitize. -
postcss,esbuild, and@babel/core(Dependabot alerts #1/#3/#4) were patched transitives held back by conservative parent pins —nextpinspostcss@8.4.31,vitedeclaresesbuild@^0.27.0, and several tools shared@babel/core@7.29.0. Pinned forward withpnpm.overrides(postcss,esbuild,@babel/core— current floors in the table above) to the patched lines. Because the first two are forced past their parents' declared ranges, both were validated end to end:pnpm build(Next.js + Tailwind exercise postcss) and the fullpnpm test:coverage(vitest is the only vite/esbuild consumer here) both pass.pnpm why <pkg>confirms a single resolved version each.
Advisories below the high gate are reported by pnpm audit but do not
block CI. They are still governed, not silent: prefer raising a floor (table
above) and, only when a fix genuinely does not exist, record the accepted risk
here with its reachability rationale so the row can be dropped when the
upstream fix lands.
| GHSA | Package | Severity | Reachability |
|---|---|---|---|
| none | — | — | pnpm audit --audit-level low reported 0 advisories after the 2026-09-04 sweep. |
.github/workflows/docker-scan.yml (required check trivy) builds the
Dockerfile and fails on any fixable HIGH/CRITICAL in the image; accepted,
non-runtime-reachable findings would go in .trivyignore with the same
rationale + review-by discipline as the table above. The runner stage deletes
the base image's bundled npm/npx/corepack/yarn CLIs, so npm's vendored
dependency tree (the source of every previous .trivyignore mute) is no longer
in the image and .trivyignore currently carries no entries. The base
image digest is tracked by Dependabot's docker ecosystem; a stale digest is
the usual cause of a base-OS finding (see docs/docker.md).
These supply-chain controls are repository settings, not files. Only a
repository administrator can change them and no check in this repository pins
them, so they are an operator checklist. The GITHUB_TOKEN a workflow
runs with cannot even read the Dependabot and Actions-permission settings:
those endpoints need administration access, and no workflow permissions:
key grants that. Confirm every row when you adopt the kit, and again after any
change to the repository's owner or security configuration (review F-28,
I-09).
| Setting | Expected | Verify (read-only) | Turn on | When it is off |
|---|---|---|---|---|
| Dependabot alerts | on | gh api -i repos/devresponse/devresponsekit/vulnerability-alerts answers 204 (404 = off) |
Settings → Advanced Security → Dependabot alerts → Enable | GitHub stops matching the lockfiles against its advisory database. The weekly Dependabot alerts job cannot read the alerts API and fails, so this one is detected after all. |
| Dependabot security updates | on | gh api repos/devresponse/devresponsekit/automated-security-fixes returns "enabled": true |
Settings → Advanced Security → Dependabot security updates → Enable, or gh api -X PUT repos/devresponse/devresponsekit/automated-security-fixes |
An advisory raises an alert and no PR. Nothing proposes the fix: the next weekly run fails and opens the tracking issue, and someone bumps or floors the package by hand. Every comment in .github/dependabot.yml that says a security update "still arrives" assumes this setting is on. |
| Actions pinned to a full-length commit SHA | required | gh api repos/devresponse/devresponsekit/actions/permissions returns "sha_pinning_required": true |
Settings → Actions → General → Actions permissions → tick Require actions to be pinned to a full-length commit SHA → Save, or gh api -X PUT repos/devresponse/devresponsekit/actions/permissions -F enabled=true -f allowed_actions=all -F sha_pinning_required=true (the call sets enabled and allowed_actions too, so pass their current values) |
Pinning is a convention only review enforces. Every uses: in .github/workflows/ is pinned today, so turning this on breaks nothing, but a uses: owner/action@v1 slipped into a later edit would run whatever that movable tag points at, and no check would fail. |
production-migrations environment: deployment branches |
main only |
gh api repos/devresponse/devresponsekit/environments/production-migrations --jq .deployment_branch_policy shows custom_branch_policies: true, and gh api repos/devresponse/devresponsekit/environments/production-migrations/deployment-branch-policies --jq '[.branch_policies[].name]' returns ["main"] |
Settings → Environments → production-migrations → Deployment branches and tags → Selected branches and tags → Add deployment branch or tag rule → branch main, or the commands below, before its secret is set |
Its one secret is production's OWNER database URL, and an environment secret is readable by any job that names environment: production-migrations, in any workflow, pushed on any branch. migrate-production.yml's own guard fences that one workflow; a workflow somebody pushes on a feature branch could still read the URL. |
production-migrations environment: required reviewers |
none | gh api repos/devresponse/devresponsekit/environments/production-migrations --jq '[.protection_rules[].type]' does not include "required_reviewers" |
Leave Required reviewers unticked | With reviewers, every merge's migrations wait for an approval while the production build's schema gate waits ten minutes for them, so an approval given later fails that build (approve, then Redeploy it in Vercel). Review belongs on the pull request; the branch policy keeps the credential on main (docs/deployment.md §1.2). |
The branch policy by API, two calls. Run them before the secret goes in: the first creates the environment if the workflow's first run has not already.
gh api -X PUT repos/devresponse/devresponsekit/environments/production-migrations --input - <<'JSON'
{ "deployment_branch_policy": { "protected_branches": false, "custom_branch_policies": true } }
JSON
gh api -X POST repos/devresponse/devresponsekit/environments/production-migrations/deployment-branch-policies \
-f name=main -f type=branchmigrate-production.yml runs on a push to main and on a manual dispatch,
which must then be started from main. Both of its jobs name the
environment, so the policy holds for the whole run. deploy.yml, which named
the production environment and needed four secrets there, was retired by
DEP2; no workflow names production any more (Vercel's Git integration still
records its deployments under it, which needs no secret and no rule).
When F-28 was verified on 2026-09-24, Dependabot alerts were on and
Dependabot security updates were off. Turning the setting on is an
operator action; no code change can do it. Security-update PRs go through the
same required checks as any other PR, and Dependabot's cooldown and
open-pull-requests-limit do not apply to them.
When I-09 was verified on 2026-09-29, SHA pinning was not required, and
the production environment (GitHub lists it as Production; the API
resolves production to it) had no branch policy, no protection rules
and no secrets. SHA pinning can go on now. The production-migrations
environment did not exist then; give it the branch policy above before its
secret (docs/deployment.md §3).
.github/workflows/secret-scan.yml runs
gitleaks (pinned container image) over every push and pull request as a
required status check on main; a hit blocks the merge. The configuration
is .gitleaks.toml and
tests/unit/gitleaks-config.test.ts
pins its policy, so a change that weakens the gate fails the unit suite before
it ever reaches CI.
Custom detection rules. The bundled gitleaks rules know nothing about the credential formats this application issues, so the config adds its own:
| Rule id | Catches | Real shape (source of truth) |
|---|---|---|
devresponse-api-key |
drk_live_ / drk_test_ API keys |
prefix + 32 base62 chars (src/lib/api-auth/api-key.ts) |
devresponse-oauth-client-secret |
drkcsec_ OAuth client secrets |
prefix + 40 base62 chars (src/lib/api-auth/oauth-clients.server.ts) |
devresponse-oauth-client-id |
drkc_ OAuth client ids |
prefix + 24 base62 chars |
devresponse-seed-default-password |
the documented seed-admin default password anywhere except the files that document it | .env.example SEED_ADMIN_PASSWORD |
devresponse-tooling-hardcoded-password |
a quoted password literal assigned in operator tooling (help/, scripts/) |
tooling reads credentials from the environment |
Only the plaintext of a key or client secret is ever shown (once); the
database holds a SHA-256 hash. A full-length value in the tree is therefore a
leak by construction, and the rules are length-bounded to exactly the real
shapes so fixtures never collide with them: keep test and documentation
values shorter than the real random segment (the unit test enforces this
across the tree) and they need no allowlisting at all. A deliberately
full-length placeholder is allowed only under tests/ or docs/ and only
when it carries an obvious marker (example, placeholder, redacted).
Allowlist policy. Allowlists are path-scoped wherever the allowed value is
a credential shape; a global regex allowlist for a credential family would make
the required check structurally blind to that family everywhere (that was the
state before the 2026-09 review, and it is what the unit test now forbids).
The seed-admin default password may appear only in the files that document
the local-only default (.env.example, docs/configuration.md,
docs/developer-onboarding.md, specs.md, CI's seed step, and the e2e
sign-in helper) — a copy in application code or tooling fails the gate.
Generated artifacts (.next/, coverage/, the UAT CSV export) and two
self-describing dummy literals (ci-only-…-not-for-production,
test-secret-test-secret-test-secret) are the only unscoped entries, and each
must still match something in the tree (dead entries only widen what the
scanner ignores). The one allowance for the app's own formats is fenced three
ways: short throwaway values (at most 12 random characters — e.g. the public
display prefix drk_live_AbCd1234) are ignored only under tests/ and
docs/, and only for the bundled generic-api-key rule; the
devresponse-* rules are never allowlisted. tests/ is never
blanket-allowlisted.
Run it locally exactly as CI does (Docker):
docker run --rm -v "$(pwd):/repo" ghcr.io/gitleaks/gitleaks:v8.30.1 \
detect --source=/repo --no-git --config=/repo/.gitleaks.toml --redactIf a finding is a false positive, prefer shortening the fixture over adding an allowlist entry; if an entry is unavoidable, scope it to the narrowest path.
Never include real secrets, production credentials, or customer data in a report. Use redacted examples. See SECURITY-adjacent configuration guidance in docs/configuration.md.