Sertaç Yıldırım field notes

Home → Engineering

Security Audits: Collecting Evidence, Not Paperwork

Last January, Monday night, 23:40. The auditors arrive on Tuesday at 09:00. Four engineers are still taking screenshots. The last request on the list: “Everyone who had access to the production database in May.” You cannot take a screenshot of May’s list today. We tried anyway.

Summary
  • The auditor does not want the control. They want proof that it worked. Writing “every change is approved” is not enough. They pick 25 random changes from the year and ask for each approval.
  • You cannot create evidence after the fact. Last year, 4 of our 6 findings were “the control worked, but we could not show it”.
  • Evidence should be a side effect of daily work. PR approvals, access group exports, deploy logs: we already produced all of them. We just did not keep them.
  • Write only the policy you can follow. “Two approvals” on paper and one approval in practice means a finding in every sample.
  • The result: this year, 141 of 186 requests (76%) were answered with a link to a folder. Engineering time went from about 260 hours to about 40, and findings from 6 to 1.

From the field: 214 requests and the last week

We have one audit season a year: the regulator’s information systems audit, and the ISO 27001 surveillance audit in the same period. Until last year we prepared for both the same way. Three weeks before the audit, the compliance team sends a spreadsheet. It has 214 rows, and every row is a request for evidence. Four engineers spend three weeks filling it in. The total cost is roughly 260 hours.

Most requests were easy: “Show the password policy configuration.” One screenshot, done. The hard ones asked about the past: “Show the approval records for these 25 production changes from the year.” For 7 of the 25, the approval was a chat message that said “ok, ship it”. Two had no approval at all. The changes were correct and had been reviewed, but there was no record that showed it.

The audit ended with 6 findings. Four were versions of the same sentence: the control worked, but there was no evidence. The other two were real, and they bothered me more. A contractor who had left still had an active account 40 days later. And we had never tested a restore from backup. If the auditor had not asked, we would not have done it for another year.

The mistake was mine. I thought the audit was the compliance team’s job, and we were just the people who sent screenshots when asked. But everything they asked about was something engineering produced: approvals, access, logs, backups. We designed the systems that could produce the evidence. We just had not designed them to do it.

A screenshot is not evidence. It is a memory. Evidence is the record the system writes itself, at the moment it happens.

What does the auditor ask?

ISO 27001, SOC 2 or a regulator’s audit: the names and control lists are different, but the logic is the same. The auditor looks at three things:

  1. Design. Is the control written down? “Every production change is approved by someone other than its author.”
  2. Implementation. Does the control really exist? Is the branch rule turned on?
  3. Operating effectiveness. Did the control work all year? This is where sampling comes in: 25 random changes, 25 approvals.

A screenshot is enough for the first two. It is not enough for the third, because the question is about the past. You cannot prove May’s access list with a screenshot in January. Either someone saved it on that day, or nobody did. The whole point is not to leave that record to someone’s memory.

Evidence as a side effect

We sat down and looked at where the evidence for each control came from. The surprise was that almost all of it was already being produced. PR approvals existed in the system. Access groups existed in the identity provider (IdP). Deploy logs existed. The problem was not producing it. It was keeping it: PRs were squashed and the approval was lost, the access group only showed its current state, and deploy logs were deleted after 30 days.

ControlOld evidenceEvidence source nowHow often
Every production change is approvedScreenshot of a chat messageDeploy log → commit → PR → approver (≠ author)Every deploy
Production access is reviewed every quarterSpreadsheet, signed PDFIdP group export + manager’s approval in a ticket; the ticket opens by itself at the start of each quarterDaily export, quarterly approval
Leavers lose access within 24 hoursList from HR, compared by handHR leaving date vs. IdP disable time; alert if the gap is over 24 hoursDaily
Direct production database access is restrictedScreenshot of the access listBreak-glass ticket + session recordingEvery session
Logs are kept for the policy periodScreenshot of the configurationDate of the oldest readable record in every log sourceMonthly
Restore from backup is testedNone (never done)Drill log: start, end, duration, check queryQuarterly

Every row in this table is an engineering decision. The compliance team can write the rule “access is removed within 24 hours”. But we choose the query that connects the HR system to the identity provider, the threshold that turns the gap into an alert, and where the record is kept.

The evidence collector: every night, one job

We tied all of it to a single nightly job. What it does is simple: for each control, query the source, write the result to a dated file, and put the file in write-once (object lock) storage. When the auditor asks for something, the answer is a folder path.

evidence-collector.yaml
# every night at 02:00. output: evidence/2025/05/14/ACCESS-01.json
# storage: write-once (object lock), kept as long as the policy says
controls:
  - id: CHANGE-01            # no unapproved production change
    source: deploy log -> commit sha -> PR -> approver
    rule:   approver != author
    breach: alert + open a ticket (24 hours if it was an emergency fix)

  - id: ACCESS-01            # who has production access
    source: idp group "prod-access" member list
    rule:   keep the list every day, write the diff separately

  - id: LEAVER-01            # leavers are disabled within 24 hours
    source: hr leaving date - idp disable date
    rule:   gap > 24 hours --> alert (on THAT day, not at the audit)

  - id: LOG-01               # log retention
    source: date of the oldest readable record in every log source
    rule:   today - oldest < policy period --> alert

The real value of the collector did not show on audit day. It showed during the rest of the year. In April, on its very first night, it found three accounts that should have been closed. The oldest had been open for 19 days. Last year the auditor found the same kind of problem, on day 40. This year we found it ourselves, on the first night.

If you learn that a control is broken from the auditor, you do not have a control. You have a calendar.

Three engineering decisions

1. An emergency path

On the day we made “the approver must not be the author” a hard branch rule, the first question came: what if someone has to ship a fix alone at 03:00? We did not loosen the rule. We wrote a second path. An emergency fix can ship without prior approval, but the deploy tool marks it, opens a ticket automatically and asks for approval within 24 hours. In 2025 there were 9 emergency releases, and all 9 were approved within 24 hours. When the auditor asked about this path, we showed the tickets. No finding.

2. Break-glass instead of permanent access

Nine people had permanent access to the production database. Every quarterly review, we asked “is this still needed?”, and every time the answer was “yes, rarely, but yes”. So we changed the question: permanent access is zero. When someone needs it, they open break-glass access with a ticket number. It closes by itself after 4 hours, and the session is recorded. In 2025, 31 sessions were opened. The reason for each one is in its ticket. The auditor’s question changed from “who had access?” to “why did they use it?”, and the answer was already written down.

3. Write only the policy you can follow

Our old policy said: “Every production change is approved by two people.” Nobody did that. An auditor checks against the written policy, not against your good intentions. Two approvals on paper and one in practice: a finding in every sample. We rewrote the policy as “one approval + all automated tests pass”, because we could produce evidence for both, 100% of the time. It looks like a weaker rule. In fact it is stronger, because now it is actually followed.

Write the policy you can follow, and follow the policy you wrote. The gap between them is what the auditor will find.

How it goes wrong

Do
  • Give every control one engineering owner, one name
  • Let the system produce evidence when the event happens
  • Keep evidence where it cannot be changed
  • Alert when a control breaks
  • Ask “where is the evidence?” in the design review of every new system
Don't
  • Treat the audit as a once-a-year project
  • Keep evidence in a wiki, a shared folder, or anywhere it can be edited
  • Write a policy stricter than what you really do
  • Leave every control as “compliance’s job”
  • Give evidence collection to an intern or a new joiner

What changed this year?

We finished the second audit in the middle of this month. The number of requests went down to 186, because the auditor closed some controls by looking at the evidence folder directly. 141 of the 186 requests were answered with a folder path. The other 45 were still collected by hand; most were policy documents and meeting notes, things that are human work by nature.

Last yearThis year
Evidence requests214186
Answered with a folder link0141 (76%)
Engineering time~260 hours, three weeks~40 hours
Change sample (with an approval record)16/2525/25
Findings61

The one finding taught us something. VPN logs were kept for 14 months, but the policy said 24. That log source was not in the collector, because we had not thought of it when we made the list. The lesson is simple: a control that is not in the collector is a control without evidence. Now, adding a line to the collector is one of the steps of setting up any new log source.

What did not work for me

  • Buying a ready-made compliance tool. That was my first reflex. The tool listed the controls nicely, but to pull evidence from our systems we still had to build every connection ourselves. The real work was the connections, not the tool.
  • Doing the quarterly review by email. We sent lists to managers and asked “do you approve?”. Half did not reply. Most of those who did said “ok” without reading the rows. We moved it to the ticket system and put a separate “keep / remove” choice on every row. In the first round, 11 access rights were removed.
  • Keeping deploy logs like application logs. A 30-day retention period was right for application logs, not for change evidence. We moved deploy records to a separate stream with its own retention period.

What to track

WhatTargetWhy
Share of controls with automatic evidenceAs high as possibleEvery piece of evidence collected by hand is work left for the last week
Age of the latest evidence, per controlNot older than its frequencyIf the collector stopped quietly, this is where you see it first
Leaving date → access removed, longest gapUnder 24 hoursThe longest, not the average; a single 40 days ruins the whole year
People approving their own PR0If the branch rule was switched off, this is how you find out
Break-glass sessions, count and lengthLow, with reasonsIf it grows, some task wants permanent access; automate that task
Last restore drillWithin 90 daysA backup you have never restored is not a backup

Checklist

Are you ready for the audit?
  • If someone picked 25 random changes from this year, for how many could I show the approval in 10 minutes?
  • Can I produce May’s production access list today?
  • If a leaver’s account stays open, who finds out, and on which day?
  • Is the evidence in write-once storage, or on a wiki page?
  • Is there a rule in our written policy that we do not follow in practice?
  • Is the approval path for emergency fixes written down, and are its tickets kept?
  • How many people have permanent access to the production database?
  • When did we last restore from backup, and how many minutes did it take?
  • Does every control have an engineering owner? What is their name?

Conclusion

On that Monday night last year, four engineers were taking screenshots at 23:40, and half of what they captured did not answer the question. The problem was not that they were not working. They were working at the wrong time. The evidence should have been collected on the day things happened.

This year, on the night before the audit, everyone was at home. The audit was not easier; the auditor asked more questions. But the answer to every question had been piling up all year, on its own, without anyone thinking about it. I described the same idea in the error budget post: write the rule on a calm day, and do not negotiate it during the fire.

The test: if the auditor came tomorrow morning, would anyone stay late tonight? If yes, you do not have evidence. You have a last-minute rush.