Security baseline
Most applications are not insecure because somebody wrote a bad line. They are insecure because a whole
domain was never considered, and nothing about the code says so. This is the list of domains, what makes
each one real rather than declared, and the order to take them in.
The rule: a control you cannot prove is a control you do not have — and a control that only new code
respects is half a control.
1. Three things that invalidate defences you already paid for
Before any list, check these. Each one silently cancels controls that appear to be in place.
- The origin must be reachable only through the edge. If the application answers requests that did not
come through the CDN, WAF or proxy, then every edge rule — bot management, rate limits, IP reputation,
the trusted client-address header — is an opt-in the attacker declines. Every control that trusts a
header set by the edge is only as true as this.
- After any credential or key exposure, rotate and then verify what the rotation broke. The application
signing key usually underwrites sessions, cookies, signed URLs and stored tokens at once; rotating it is
correct and it invalidates all four. Rotating without checking them is how a fix becomes an outage;
not rotating is how an incident stays open.
- A language with no static analysis in the pipeline has no automated floor. If the main language of
the codebase is not covered by a scanner, every finding in it depends on someone noticing. Know which
languages your pipeline actually covers — the default configuration of most tools does not cover all of
them.
2. The domains
Walk them in this order the first time. references/controls.md has the individual controls for each, with
what makes them real; read the section for the domain you are working on, not the whole file.
| # |
Domain |
The question it answers |
| 1 |
Access control and authorisation |
Can one identity reach another's data or actions? |
| 2 |
Authentication and identity |
Can somebody become a user they are not? See padosoft-auth-hardening |
| 3 |
Injection |
Can input become code, a query, a command or markup? |
| 4 |
Cryptography and secrets |
Where do the secrets live, who can read them, when do they change? |
| 5 |
Configuration |
Is a safe default actually the default, everywhere, in every environment? |
| 6 |
Software supply chain |
Do you know what you ship, and would you notice if it changed? |
| 7 |
Build and delivery integrity |
Can somebody put code into production without going through the gate? |
| 8 |
Logging, detection and alerting |
If it happens tonight, who finds out, and how? |
| 9 |
Exceptional conditions |
When something fails, does it fail closed? |
| 10 |
Rate limiting and anti-automation |
What stops a script doing this ten thousand times? |
| 11 |
Business-logic abuse |
What happens if somebody uses the feature exactly as designed, maliciously? |
| 12 |
Data protection and privacy |
What do you hold, for how long, and who else gets it? |
| 13 |
Uploads and files |
Can a file become an execution path? |
| 14 |
API surface |
Which endpoints exist, and does each one carry its own authorisation? |
| 15 |
Browser surface |
What runs on your origin, and who put it there? |
| 16 |
Mobile surface |
What did you ship inside a bundle that anybody can open? |
| 17 |
Infrastructure and edge |
What is reachable, with which privileges, and what happens when it is lost? |
| 18 |
Process and governance |
What keeps all of the above true in six months? |
| 19 |
AI and model surface |
The model is a caller with unusual reach — see padosoft-agent-host-boundaries |
Domains 1, 3, 9, 13 and 14 are where a review of the code finds things. Domains 5, 6, 7, 17 and 18 are where
an assessment of the system finds things, and they are the ones that get skipped because no diff contains
them.
3. Reading a status honestly
The whole value of an assessment is in refusing to overstate it. Four statuses, and the discipline is in the
last two:
|
Means |
| Covered |
there is a control, and a gate or a rule applies it to new code |
| Native |
the framework provides it unless somebody disables it — the risk is deactivation, not absence |
| Partial |
it exists and does not cover the whole surface. Write what is missing, not just "partial" |
| Not inspected |
nobody looked. Write how to look, so the next person can. This is an honest state |
Two traps that turn an assessment into fiction:
- "Covered by a rule" does not mean the existing codebase complies. A rule is applied in review on new
code. Code written before the rule existed has never been checked against it. Both facts are fine;
conflating them is not.
- An identifier quoted in a comment is not a control. Code that says it implements a security rule, with
no rule behind it, means the control exists today and nothing prevents the next change from removing it.
The inverse of the previous trap: there, old code is unverified; here, future code is unguarded.
4. The rules that recur in every domain
These are not domain-specific. They decide whether a control is real.
- A setting is not a boundary. An authorisation or security boundary must not be switchable at runtime
by anybody who can reach a settings screen. If it must be configurable, the configuration cannot reach
off, and the floor is in code.
- An empty configuration value must not mean "disabled". The absent case is the common case: a fresh
environment, a tenant nobody configured, a variable with a typo. Absent means the secure default, and
disabling takes an explicit, visible value.
- A control that fails open needs a written reason and a second control behind it. Failing open is
sometimes right — refusing every login because a third-party verifier is unreachable is worse than the
friction it was buying. It is only acceptable while something else carries the weight, and that
dependency is written down so that removing the other control re-opens the question.
- A control that fails closed needs an escape. A guard that denies on its own internal error is correct
and will one day deny everybody; know which of the two you chose, and why.
- Half a switch is worse than none. A toggle that disables the server-side check while the client keeps
behaving as if the control were on gives the impression of a decision nobody actually made.
- Twin endpoints must be protected identically. Where two routes reach the same resource — an older and
a newer one, a page and its asynchronous counterpart, a web form and an API — the one built last usually
has the controls, and the one built first is still wired.
- A gate that is born red and left red gets switched off within a week, and then it protects nothing at
all. Introduce a strict check on new work first, and bring the backlog in behind it.
- Remove an endpoint rather than gating it. A route that exists in order to be refused is a route that
one configuration change makes reachable.
5. Turning a finding into a control
A closed finding is worth very little; the class it belongs to is worth a lot.
- Fix it in a small change, with a test that fails without the fix.
- Name the class. Not "this endpoint leaked the model name" but "error messages must not carry internal
identifiers".
- Write the rule, and put the check where it will run without anyone remembering — a validator, a
grep in review, a test, a gate.
- Label the change, so the set of security work is answerable in one query when an auditor, a customer
or your own team asks what was done and what is still open. A security fix that stays open is an exposure
window, not a backlog item.
- Find the twins. The same class almost always exists somewhere else in the codebase.
6. Where to start, on a new application
In order, because each one makes the next cheaper:
- Authorisation derived from the authenticated identity, never from a parameter —
padosoft-tenant-isolation.
- Errors that carry nothing internal, and logs that carry nothing personal —
padosoft-logging-discipline.
- Rate limiting on everything that can be attempted repeatedly —
padosoft-auth-hardening.
- Secure defaults for headers, cookies and the environment gate —
padosoft-environment-gating.
- Dependency and secret scanning in the pipeline, blocking on the diff —
padosoft-ci-workflow-gates.
- One audit trail whose integrity can be shown —
padosoft-evidence-boundaries.
- Only then the long tail in
references/controls.md.
Gotchas
- A penetration test is a sample, not a coverage measure. Closing its findings raises the floor by
exactly the height of that sample.
- The report that arrives too often is not read. A monthly access review gets attention; a weekly one
becomes a filter rule — see
padosoft-failure-visibility.
- The control everybody forgets is the one for privileges granted outside the role system. Reviewing
roles does not show them, and they are the channel through which access grows silently.
- A configuration flag and an environment name are two gates, and the dangerous arrangement is the one
where either alone turns a convenience on.
- Owning a scanner is not running it, and running it is not reading it. The finding with no owner and no
date is the same as no finding.
- An exception in an allow-list outlives the thing that justified it. When that name comes back, the
exception authorises it silently. Expire them, or report the orphans.
Checklist
Final report
Scope: <application / service> · assessed against: <standards>
Domains: covered <n> · native <n> · partial <n> · not inspected <n>
Invalidators: edge-only origin <…> · key rotation <…> · static analysis <…>
Top gaps, in the order to take them: 1. <…> 2. <…> 3. <…>
Rules added from findings: <which, and where the check runs>
Stated honestly: <what is "partial" and why> · <what nobody looked at>
1---2name: padosoft-security-baseline3description: Use this skill when deciding what security an application must have — starting a new project, taking one to production, preparing for an audit, a penetration test or a customer questionnaire, planning a hardening backlog, or answering "are we secure enough?". Also when the user asks which controls are missing, what to do first, how to prove a control exists, or wants a new service to start at the same level as an existing one. It walks the control domains, separates what the framework gives you from what you must build, and turns a finding into a control that holds for future code. Do not use it to review a specific diff — the per-stack review skills do that — nor to run a scanner or write a threat model for one feature.4license: MIT5---67# Security baseline89Most applications are not insecure because somebody wrote a bad line. They are insecure because a whole10**domain** was never considered, and nothing about the code says so. This is the list of domains, what makes11each one real rather than declared, and the order to take them in.1213**The rule: a control you cannot prove is a control you do not have — and a control that only new code14respects is half a control.**1516---1718## 1. Three things that invalidate defences you already paid for1920Before any list, check these. Each one silently cancels controls that appear to be in place.21221. **The origin must be reachable only through the edge.** If the application answers requests that did not23 come through the CDN, WAF or proxy, then every edge rule — bot management, rate limits, IP reputation,24 the trusted client-address header — is an opt-in the attacker declines. Every control that trusts a25 header set by the edge is only as true as this.262. **After any credential or key exposure, rotate and then verify what the rotation broke.** The application27 signing key usually underwrites sessions, cookies, signed URLs and stored tokens at once; rotating it is28 correct and it invalidates all four. Rotating without checking them is how a fix becomes an outage;29 *not* rotating is how an incident stays open.303. **A language with no static analysis in the pipeline has no automated floor.** If the main language of31 the codebase is not covered by a scanner, every finding in it depends on someone noticing. Know which32 languages your pipeline actually covers — the default configuration of most tools does not cover all of33 them.3435## 2. The domains3637Walk them in this order the first time. `references/controls.md` has the individual controls for each, with38what makes them real; read the section for the domain you are working on, not the whole file.3940| # | Domain | The question it answers |41|---|---|---|42| 1 | Access control and authorisation | Can one identity reach another's data or actions? |43| 2 | Authentication and identity | Can somebody become a user they are not? See **`padosoft-auth-hardening`** |44| 3 | Injection | Can input become code, a query, a command or markup? |45| 4 | Cryptography and secrets | Where do the secrets live, who can read them, when do they change? |46| 5 | Configuration | Is a safe default actually the default, everywhere, in every environment? |47| 6 | Software supply chain | Do you know what you ship, and would you notice if it changed? |48| 7 | Build and delivery integrity | Can somebody put code into production without going through the gate? |49| 8 | Logging, detection and alerting | If it happens tonight, who finds out, and how? |50| 9 | Exceptional conditions | When something fails, does it fail closed? |51| 10 | Rate limiting and anti-automation | What stops a script doing this ten thousand times? |52| 11 | Business-logic abuse | What happens if somebody uses the feature exactly as designed, maliciously? |53| 12 | Data protection and privacy | What do you hold, for how long, and who else gets it? |54| 13 | Uploads and files | Can a file become an execution path? |55| 14 | API surface | Which endpoints exist, and does each one carry its own authorisation? |56| 15 | Browser surface | What runs on your origin, and who put it there? |57| 16 | Mobile surface | What did you ship inside a bundle that anybody can open? |58| 17 | Infrastructure and edge | What is reachable, with which privileges, and what happens when it is lost? |59| 18 | Process and governance | What keeps all of the above true in six months? |60| 19 | AI and model surface | The model is a caller with unusual reach — see **`padosoft-agent-host-boundaries`** |6162Domains 1, 3, 9, 13 and 14 are where a review of the code finds things. Domains 5, 6, 7, 17 and 18 are where63an assessment of the *system* finds things, and they are the ones that get skipped because no diff contains64them.6566## 3. Reading a status honestly6768The whole value of an assessment is in refusing to overstate it. Four statuses, and the discipline is in the69last two:7071| | Means |72|---|---|73| **Covered** | there is a control, and a **gate or a rule applies it to new code** |74| **Native** | the framework provides it unless somebody disables it — the risk is deactivation, not absence |75| **Partial** | it exists and does not cover the whole surface. Write **what is missing**, not just "partial" |76| **Not inspected** | nobody looked. Write **how** to look, so the next person can. This is an honest state |7778Two traps that turn an assessment into fiction:7980- **"Covered by a rule" does not mean the existing codebase complies.** A rule is applied in review on new81 code. Code written before the rule existed has never been checked against it. Both facts are fine;82 conflating them is not.83- **An identifier quoted in a comment is not a control.** Code that says it implements a security rule, with84 no rule behind it, means the control exists *today* and nothing prevents the next change from removing it.85 The inverse of the previous trap: there, old code is unverified; here, **future** code is unguarded.8687## 4. The rules that recur in every domain8889These are not domain-specific. They decide whether a control is real.9091- **A setting is not a boundary.** An authorisation or security boundary must not be switchable at runtime92 by anybody who can reach a settings screen. If it must be configurable, the configuration cannot reach93 *off*, and the floor is in code.94- **An empty configuration value must not mean "disabled".** The absent case is the common case: a fresh95 environment, a tenant nobody configured, a variable with a typo. Absent means the **secure default**, and96 disabling takes an explicit, visible value.97- **A control that fails open needs a written reason and a second control behind it.** Failing open is98 sometimes right — refusing every login because a third-party verifier is unreachable is worse than the99 friction it was buying. It is only acceptable while something else carries the weight, and that100 dependency is written down so that removing the other control re-opens the question.101- **A control that fails closed needs an escape.** A guard that denies on its own internal error is correct102 and will one day deny everybody; know which of the two you chose, and why.103- **Half a switch is worse than none.** A toggle that disables the server-side check while the client keeps104 behaving as if the control were on gives the impression of a decision nobody actually made.105- **Twin endpoints must be protected identically.** Where two routes reach the same resource — an older and106 a newer one, a page and its asynchronous counterpart, a web form and an API — the one built last usually107 has the controls, and the one built first is still wired.108- **A gate that is born red and left red gets switched off within a week**, and then it protects nothing at109 all. Introduce a strict check on new work first, and bring the backlog in behind it.110- **Remove an endpoint rather than gating it.** A route that exists in order to be refused is a route that111 one configuration change makes reachable.112113## 5. Turning a finding into a control114115A closed finding is worth very little; the class it belongs to is worth a lot.1161171. **Fix it** in a small change, with a test that fails without the fix.1182. **Name the class.** Not "this endpoint leaked the model name" but "error messages must not carry internal119 identifiers".1203. **Write the rule**, and put the check where it will run without anyone remembering — a validator, a121 grep in review, a test, a gate.1224. **Label the change**, so the set of security work is answerable in one query when an auditor, a customer123 or your own team asks what was done and what is still open. A security fix that stays open is an exposure124 window, not a backlog item.1255. **Find the twins.** The same class almost always exists somewhere else in the codebase.126127## 6. Where to start, on a new application128129In order, because each one makes the next cheaper:1301311. Authorisation derived from the authenticated identity, never from a parameter — **`padosoft-tenant-isolation`**.1322. Errors that carry nothing internal, and logs that carry nothing personal — **`padosoft-logging-discipline`**.1333. Rate limiting on everything that can be attempted repeatedly — **`padosoft-auth-hardening`**.1344. Secure defaults for headers, cookies and the environment gate — **`padosoft-environment-gating`**.1355. Dependency and secret scanning in the pipeline, blocking on the diff — **`padosoft-ci-workflow-gates`**.1366. One audit trail whose integrity can be shown — **`padosoft-evidence-boundaries`**.1377. Only then the long tail in `references/controls.md`.138139---140141## Gotchas142143- **A penetration test is a sample, not a coverage measure.** Closing its findings raises the floor by144 exactly the height of that sample.145- **The report that arrives too often is not read.** A monthly access review gets attention; a weekly one146 becomes a filter rule — see **`padosoft-failure-visibility`**.147- **The control everybody forgets is the one for privileges granted outside the role system.** Reviewing148 roles does not show them, and they are the channel through which access grows silently.149- **A configuration flag and an environment name are two gates**, and the dangerous arrangement is the one150 where either alone turns a convenience on.151- **Owning a scanner is not running it, and running it is not reading it.** The finding with no owner and no152 date is the same as no finding.153- **An exception in an allow-list outlives the thing that justified it.** When that name comes back, the154 exception authorises it silently. Expire them, or report the orphans.155156## Checklist157158- [ ] Origin reachable only through the edge; every header-trusting control depends on it159- [ ] Keys rotated after any exposure, and what the rotation invalidated has been verified160- [ ] Static analysis covers the main language of the codebase161- [ ] All 19 domains have a status, and *not inspected* says how to inspect162- [ ] No status claims coverage of the existing codebase when the rule only applies to new code163- [ ] No security boundary governed by a runtime setting; no empty value meaning "off"164- [ ] Every fail-open documented with the control that carries the weight165- [ ] Twin endpoints compared side by side166- [ ] Each closed finding has a named class, a rule and a check that runs by itself167- [ ] Security changes labelled so the whole set is answerable in one query168169## Final report170171```172Scope: <application / service> · assessed against: <standards>173Domains: covered <n> · native <n> · partial <n> · not inspected <n>174Invalidators: edge-only origin <…> · key rotation <…> · static analysis <…>175Top gaps, in the order to take them: 1. <…> 2. <…> 3. <…>176Rules added from findings: <which, and where the check runs>177Stated honestly: <what is "partial" and why> · <what nobody looked at>178```