Christian Lehnert — Linux, Hacking & Faith

Security by Design - The Phrase Is Worthless, the Practice Is Not

Christian Lehnert2026-07-18~9 min read

Security by Design

What I Actually Find

The findings I write up as a pentester are almost never clever. Every
so often there is a genuinely interesting bug, a race condition or a
parser differential that took real work to reach. Those are the ones
I enjoy and they are a small minority.

The rest are structural. An endpoint that trusts a client-supplied
identifier because the design assumed the client was the frontend.
A service account with database-admin rights because at some point
somebody needed a migration to run and never scoped it back down. A
trust boundary that exists in the architecture diagram and does not
exist in the code, because the validation was supposed to happen at
the gateway and the gateway was bypassed by an internal caller
nobody documented. Input that is sanitized in four places and not in
the fifth.

None of these are implementation mistakes in the sense of "somebody
wrote a bad line of code." They are design decisions, made two or
three years ago, in a sprint planning meeting where nobody asked the
security question, and by the time I show up they are load-bearing.
The report I write says "fix this," and what fixing it actually means
is unwinding an assumption that half the system now depends on.

That is the entire case for Security by Design, and it is not the
case that the phrase makes.

The Phrase Is Worthless

"Security by Design" has been drained of meaning by roughly fifteen
years of consultants putting it on slides. Every organization claims
to do it. It appears in every vendor deck, every compliance
questionnaire, every RFP response. It has the same semantic weight as
"customer-centric" or "data-driven," which is to say none, because
nobody has ever said they were against it.

The claim is cheap because it is unfalsifiable at the level it is
usually made. What would it look like to not do security by design?
Nobody deliberately designs insecurely. The teams shipping the
systems I break into all believed they were building securely. They
were not lying. They were doing what everybody does: making a
thousand design decisions under deadline pressure, most of which had
a security dimension nobody surfaced, and calling the result "secure
by design" because they had a security review at the end.

The gap between the slogan and the practice is where all the actual
content is. So let me try to say what the practice actually consists
of, in terms concrete enough to be wrong.

What It Actually Means, Concretely

Secure defaults beat added controls. This is the deepest lever
and the one most often skipped in favor of buying a tool. A control
is something you add on top of a system that would otherwise be
insecure; a default is a design choice that makes the insecure thing
hard to do. A framework where the ORM parameterizes queries by
default does more for SQL injection than any amount of SAST scanning,
because it removes the failure mode instead of detecting it. A
service that requires an explicit allow-list to reach the network
prevents more lateral movement than a monitoring rule that alerts on
it. The design question is not "what control do we add" but "what
default makes the dangerous path the one you have to work for."

Trust boundaries are a design artifact or they do not exist. Every
system has places where data crosses from a context you do not
control into one you do. If those crossings are not identified,
named, and assigned an owner at design time, they will not be
validated consistently, because there is no single place that knows
they are boundaries. Almost every injection finding I write is a
boundary that existed conceptually and not structurally. Draw them.
Put the validation at them, not near them. Make it impossible to
reach the trusted side without passing through.

Failure must degrade toward the conservative answer. When a
component times out, when a check is unavailable, when the expensive
validation cannot run, what does the system do? If the answer is
"whatever the code happens to do when the exception is swallowed,"
then your failure mode is a coin flip and it lands on "permissive"
more often than you would like. This is a design decision, it takes
five minutes to make deliberately, and almost nobody makes it. Ask,
for every check in the system: what happens when this does not
answer? Make the answer "the strict thing."

Least privilege is designed, not retrofitted. Nobody has ever
successfully scoped down a service account's permissions after the
fact, because by then nine things depend on the excess and nobody
knows which. The privilege a component gets is set at the moment it
is created, and every subsequent widening is permanent. This is why
the machine identities I find in engagements have permissions that
made sense for one migration in 2022 and have been a standing
lateral-movement path ever since.

Dependencies are design decisions. The choice to pull in a package
is a choice to run its maintainers' code with your privileges, to
inherit its vulnerability surface, and to depend on its support
lifecycle. That is an architectural commitment, and it is routinely
made by a single developer in a single commit with no review of what
it implies. The supply-chain compromises of the last two years all
exploited this, and none of them required novel technique. They
required somebody to have added a dependency without treating it as
an architectural decision.

Why It Fails Anyway

If the practice is this legible, why is it so rare? Four reasons, and
none of them are "developers do not care."

Security is a non-functional requirement competing against features
that have deadlines and a product manager. The feature ships or it
does not, and everyone sees which. The threat model that was not
written produces no visible artifact. In a contest between a thing
that gets noticed and a thing that does not, under time pressure,
the thing that gets noticed wins every time. This is not a character
flaw, it is an incentive structure.

The person who makes the design decision does not feel its
consequence. The architect who lets the internal service skip
authentication because "it is on the private network" is not the
person who, three years later, spends a weekend on the incident
response when that assumption fails. The cost lands somewhere else,
later, on someone else. Every incentive in the moment points toward
the shortcut.

"Shift left" became "dump scanners on developers." The industry took a
good idea — catch problems earlier — and implemented it as SAST and
dependency scanners bolted into CI, producing a volume of findings
with a false-positive rate high enough that developers learned to
ignore the entire category. A scanner that cries wolf four times a
day has not shifted security left. It has trained your engineers that
security findings are noise. This is worse than no scanner, because
now the real finding arrives in a channel everybody mutes.

And threat modeling, done as a one-time workshop before the project
starts, is theater. The system you threat-modeled is not the system
you shipped. The threat model that matters is the one you do on the
change, continuously, at the pull request, when somebody adds an
endpoint or a dependency or a new trust relationship. That is a habit,
not a workshop, and habits are much harder to buy than workshops.

The Part That Changes the Calculus

I would normally end a post like this with an appeal to
professionalism, which never works, because as established the
incentives point the other way.

What is actually changing the calculus is regulation, and Swiss
engineers shipping into the EU should be paying attention, because
the deadline is close. The EU Cyber Resilience Act entered into force on
10 December 2024, with the bulk of its obligations applying from 11
December 2027. But from 11 September 2026, manufacturers must report
actively exploited vulnerabilities and severe incidents affecting
products with digital elements: an early warning within 24 hours of
becoming aware, a full notification within 72 hours, and a final report
within 14 days of a fix being available. That is under two months away
as I write this.

The scope is wide. It covers essentially any hardware or software
product with digital elements placed on the EU market, and the reporting
obligations apply retroactively to products already on the market — it
does not matter if your product shipped in 2019, if it is still in use
and contains an actively exploited vulnerability, you must detect it and
report it. The penalties are GDPR-scale: up to €15 million or 2.5% of
worldwide annual turnover for failing to meet the core cybersecurity
requirements.

And the December 2027 tranche is where the phrase becomes a legal
obligation. Manufacturers must perform comprehensive risk assessments
during the design and development phases, define a support period of at
least five years, and provide security patches free of charge. SBOMs
become mandatory. "Security by design" stops being a claim you make in a
deck and becomes a thing you have to be able to demonstrate to a market
surveillance authority.

I have mixed feelings about this, and I will state them plainly.
Regulation is a blunt instrument and it will generate an enormous
amount of compliance theater — documentation produced to satisfy an
auditor rather than to make anything safer, consultants selling
"CRA readiness" packages that are slide decks. That is the tax, and
it is real.

But the thing regulation does that appeals to professionalism does
not is change the incentive structure. The design decision that used
to be free — skip the auth on the internal service, ship with the EOL
dependency, do not bother scoping the service account — now has a
price attached that lands on the organization rather than on some
future incident responder. The 24-hour reporting clock in particular
forces something the industry has avoided for two decades: you have
to actually know what is in your product and whether it is being
exploited, which means you need an SBOM, and vulnerability
monitoring, and an internal escalation path that works. Those are
not compliance artifacts. Those are the things you needed anyway.

Closing

The reason security work is expensive is that it is almost always
being done at the wrong end of the lifecycle. By the time I am
testing your system, the design decisions are load-bearing, the
service account permissions have nine consumers, the trust boundary
is a diagram rather than a code path, and the fix I am recommending
is a refactor you cannot schedule.

The cheapest security work in the history of your product happened
two years before I showed up, in a meeting nobody thought was about
security, when somebody either did or did not ask what happens when
this input is hostile, what this component actually needs permission
to reach, and what the system does when this check does not answer.
Those three questions, asked routinely at design time, prevent more
findings than any tool I have ever run.

The phrase is worthless. Ask the questions.

Tagged:
#security #sdlc #threat-modeling
← Back to posts