Most of the security work I get called into arrives about six weeks before an assessment. Somebody has a deadline, a control list, and a system built without reference to either. I take the work, but it is remediation, and remediation is the most expensive security engineering there is, because every fix has to be retrofitted into decisions that are already load-bearing.

The alternative comes down to a handful of choices made during design that cost almost nothing then and a great deal later. I want to be specific about which ones, because vague advice to build security in is why nobody does it.

Threat modeling that takes an afternoon

You need a data flow diagram, the people actually building the thing, and two hours. No methodology with a certification attached.

Draw the system, then draw the trust boundaries: every place data crosses from something you control to something you do not, or from one privilege level to another. For each boundary, ask what an attacker on the far side could do, and write down what you decided about it. That last part is where the value is. Half the entries will read “we accepted this risk because the exposure is limited to X,” and the written record of that decision, with a date and a name on it, is worth more to you at assessment time than any control you implement.

Assessors are mostly testing whether you have a process that produces reasoned decisions, and evidence that the process ran. A design-phase threat model with dated revisions answers that better than anything else, and one produced the week before the assessment fools nobody.

On healthcare work with regulated patient data, the boundary that deserves the most attention is between clinical staff and the records of patients they are not treating. It is the everyday case, it is where breach notifications actually come from, and it is almost never on the diagram unless somebody insists.

Access control decisions with a steep cost curve

Here is my single strongest recommendation, and I will defend it against the usual objections.

Put the tenancy or ownership column on the table at creation time, before you have a use case for it, and make every query in the system scoped by it at construction rather than filtered after retrieval.

The cost of doing this on day one is one column and a query helper. The cost of doing it in year three is a schema migration on a live table, a backfill against records whose ownership has to be inferred, an audit of every query in the codebase, and a period during which you cannot honestly answer whether the segregation holds. I have watched clinical systems face exactly that bill. The estimate is always wrong by a factor of three.

The second decision is where authorization lives. Put it in one place: a function that takes an actor, an action, and a resource and returns a decision. Not scattered through controllers, and never enforced in the user interface, which is a rendering concern rather than a security control. When authorization is one function, an assessor can read it, you can test it exhaustively, and adding a role later is an afternoon rather than an archaeology project.

Deny by default, everywhere. New endpoints and new fields stay inaccessible until somebody grants access on purpose. The usual failure runs the other way: a field gets added to a serializer and becomes visible to every role that could already see the parent object, because exposing it required no decision from anyone.

People call this over-engineering for a small system. The small system is the one that grows without a redesign, and scoping a query by owner is three lines whether you have four users or forty thousand.

Logging for debugging versus logging for evidence

These are different systems that share a verb, and conflating them is the most common reason a logging control fails at assessment despite the team having plenty of logs.

Debug logging is for engineers: free-form, high volume, sampled when it gets expensive, short retention, mutable. It is useful and it should exist.

Evidence logging is for a reader who was not there and does not trust you. It has different properties, and every one of them is a design decision:

  • Structured, with a stable schema and a version field. An assessor, and more importantly your own incident responder, has to query these. Free text does not answer “show me every access to this record in March.”
  • Actor and impersonator recorded separately. When an administrator acts on behalf of a user, both identities go in the event. Systems that collapse this are unable to answer the question that support-tool abuse investigations turn on.
  • Reads logged, not only writes. For regulated data this is the whole point. The unauthorized-access case leaves no trace in a write-only audit trail.
  • Written to somewhere the application cannot delete from. Append-only storage, or shipped off the host within seconds. If the process that gets compromised can rewrite the log, the log is a narrative rather than evidence.
  • Time synchronized, with the timezone recorded. Correlating three systems whose clocks disagree by ninety seconds is miserable during an incident.
  • Retention set by written policy. Somebody has to decide the period rather than inheriting whatever the log shipper defaults to.

I know how expensive it is to skip this, because I have been on the wrong side of it. On a compromised WordPress install I worked through recently, the attacker’s output was still sitting in the database and could be described in full. How they got in is permanently unanswerable, because the host’s access logs had rotated and nothing had been shipped anywhere. The logging that would have answered the question costs a few dollars a month. Not having it turned a two hour investigation into a permanent gap in the record.

The objection to evidence logging is storage cost. Storage is cheap next to reconstructing an incident from application logs you rotated at seven days, and cheaper still than telling a client you cannot determine scope.

The layer that keeps losing

In a small organization the weakest control is the staff, for reasons of arithmetic rather than character. A twelve-person company has no separation of duties available, because there are not enough people to separate. Everyone recognizes the principal’s voice, and a voice is now something that can be synthesized from a conference recording.

I have run social engineering training for client staff: phishing, pretexting, impersonation. What I concluded from doing it:

Simulated phishing click rates are a weak metric, and they turn harmful the moment anybody is punished for a click, because you get people who quietly stop reporting. Measure report rate and time to first report instead. A team whose first report arrives in four minutes is in decent shape regardless of how many people clicked.

Pretexting works because it targets helpfulness rather than ignorance. The caller knows the org chart, uses a real project name, and has a plausible urgency. Training people to be suspicious does not scale, since the job requires being helpful to strangers. A procedure scales: certain requests get verified out of band, on a number from the internal directory, every time, with no exception for seniority. Payment detail changes. MFA resets. Requests for access on behalf of somebody else. Anything where the requester is in a hurry and the action is hard to reverse.

The procedure only works if people have cover. Say out loud, from the top, that refusing an unverified request is correct behavior and will never be held against anyone, including when the request came from me. Otherwise it lives on paper and gets skipped the first time a vice president is annoyed.

Maintenance debt is a control failure

Every organization has systems nobody owns: an old reporting tool, a test environment holding production data, a marketing site running a content platform. They stay off the risk register because the register gets written from memory, and they never get patched because patching is somebody’s job only once somebody is named.

Our own compromised site is the example I use now, and it is more persuasive for being mine. Nothing about that failure was clever. No control was bypassed, because no control existed. A system had no owner, which is a condition with a predictable outcome and an unpredictable date.

Keep an inventory with an owner column and no blank cells. Anything that cannot be assigned an owner gets a decommission date. That is the cheapest security control I know of, and the one most often missing.

Making the assessment cheap

If you do the above, the assessment stops being a project. Keep a compliance/ directory in the repository, one file per control, each naming the code paths that implement it and holding a script that produces current evidence. Generate evidence by running something. Screenshots are stale the moment they are captured, and an experienced assessor can tell.

People call this bureaucracy. It runs to about a day of work spread across a project, it lives next to the code so it goes stale visibly, and the alternative is the six-week remediation engagement I described at the top, which I am happy to sell you and would rather you did not need.