GitOps as an audit trail: why a Git log beats a ticket system
Table of Contents
Every audit of an IT system asks the same thing: can you prove who changed what, when, and with whose approval? Most organisations answer with tickets and screenshots, and collect evidence by hand in the weeks before the auditor arrives. There is a better answer, and it comes as a side effect of how modern infrastructure gets deployed. This post explains that approach, called GitOps, and shows why the record it produces is stronger evidence than a ticket system can give you.
Unlike most of my other posts, this one needs no technical background to follow it.
In short #
- With GitOps, every change to a system goes through a change history that records what changed, who made it and when.
- Signed changes, enforced reviews and closed access turn the change history into the evidence, and that evidence beats a ticket.
- A work item holds the intent, and the linked commits show what changed, so anyone can check one against the other.
- The evidence comes as a by-product of what you already do, so nobody gathers it by hand before an audit.
- Emergency changes and drift still need explicit handling, but you don’t have to lose the evidence trail for it.
What an auditor wants from change management #
Strip the vocabulary away and change management asks five questions about every change to a production system:
- What changed?
- Who made the change?
- When did it happen?
- Who approved it?
- Did what ended up in production match what was approved?
A ticket system answers the first four on paper. It answers the fifth by trust. Someone writes “upgrade the database” in a ticket, someone else approves it, and an engineer then goes into production and does something they believe matches. The ticket and the change are two separate records held together by habit. Nothing stops the engineer from doing slightly more, or something different, or nothing at all and closing the ticket anyway.
Ticket systems record intent well. Their weakness is that intent and action live in different places, so the evidence is only as good as the discipline of the person connecting them.
GitOps in plain terms #
Git is a version-control system. Software teams use it to store their work in a repository, which keeps every change ever made to it as a numbered entry called a commit. Each commit records what changed, who made it and when. Nothing is overwritten. The full history stays.
GitOps takes that idea and applies it to running infrastructure. You describe how you want your systems to look in files in a repository: which applications run, which versions, with which settings. A piece of software watches the repository and makes the real systems match it. Nobody logs in to a server to make a change. They change the files, and the software applies the change.
Two consequences matter for audit. The repository always describes what should be running, and the only way to change what runs is to change the repository. That single rule turns the change history into the record of every change to production.
I set this up on my own platform, and the earlier post on FluxCD and ArgoCD covers the tools and how they hand control to Git. The rest of this post needs none of that detail, so read it if you want the technical details and want to run this yourself.
Why Git history is stronger evidence #
In GitOps, the change and the record of the change are the same object. The engineer doesn’t describe the change and then make it. They make the change in the repository, and the systems follow. Each property an auditor cares about falls out of that.
What changed is the difference between the old files and the new ones, line by line. Nobody can summarise it wrongly because nobody summarises it. When is the commit time. Who is the author, and with signing turned on (more on that below) that is a cryptographic claim instead of a name in a text field.
Two properties of Git go beyond what a ticket is able to give you. First, rewriting history changes every commit hash after the edit, so anyone holding a copy sees the mismatch. A ticket system lets an administrator edit a field and leave no trace. Second, the change in the repository is the change applied to the systems, so the gap between “what was approved” and “what happened” shrinks to what the GitOps software does. That is far less to verify than every engineer with production access.
Closing the loopholes #
A change history is only as trustworthy as the rules around it. At default settings, anyone with write access can push changes straight in, write commits under any name and overwrite history. Three controls fix that.
Step 1: Require a second person to approve #
Teams normally propose a change as a pull request: a request to merge a set of commits into the main copy of the files, which gives colleagues a place to review it first. Branch protection turns that habit into a rule. Nothing reaches the main copy without a pull request, and the pull request needs an approval before it merges. That gives you the fourth question, who approved, as a recorded event instead of a chat message.
I decide who has to approve with a CODEOWNERS file. It is a plain file in the repository that maps parts of the system to the people who must review changes to them. Some parts of infrastructure or application need a different reviewer than another part, and the file says so in a place that is itself under version control. A change to the file goes through review like everything else, so nobody can quietly remove themselves from the approval path.
I keep a privileged break-glass role that can force a change through, because review stalls when only one person is available. Important: every forced change is still recorded under the identity of whoever forced it, so the override shows up in the history as an exception you can explain. Limit that role to as few people as possible and only use it for emergency situations.
Step 2: Sign every commit with a key that lives on hardware #
An author name in Git is free text. Anyone can set it to anyone else’s. Commit signing replaces the claim with proof. The commit carries a digital signature that only the holder of a private key can produce, and the hosting platform marks it as verified when the signature matches a key registered to that person.
I sign with GPG, and the private key lives on a hardware security key, a small USB device that holds the key and never lets it leave. A key stored as a file on a laptop can be copied, and a copied key means the signature proves nothing about who was at the keyboard. A key on a hardware token can’t be extracted, and I set mine to require a pin code and a physical touch every time I commit. Each signed commit is then evidence that a specific person was present when it was made.
If you want the background on how hardware keys and digital signatures work, I wrote about why hardware security keys matter. This is that argument applied to change records.
The repository can reject unsigned commits, which turns signing from a habit into a rule. Pay attention to who performs the merge. The merge is a recorded action under the identity of whoever did it, so the person who merges a change is the one on record as accountable for it reaching the next stage (e.g., production). Important: all of this depends on named identities that belong to real people. A shared or generic account breaks the trail, because the record then names nobody.
Step 3: Make Git the only way in #
Signed, reviewed changes prove nothing if someone can alter the systems. So some doors need closing. Access to the infrastructure follows strict role-based rules: people get read access, and only the GitOps software can write to the things it manages. On top of that, automated policies refuse changes to those things from anyone but that software, so a person with a stray write permission still gets rejected.
This pattern is important because a review process people can route around is not proving anything. With access rules and policies in place, there is only one path to follow. It also shortens the audit conversation. “Can someone change production without a reviewed commit?” gets the answer “no, and here is the rule that prevents it”.
Promotion is a change too #
New versions of software normally move through stages. A version gets built, tested in a test environment and then promoted towards production. In a ticket process each promotion is another ticket or a comment on one.
I use a tool called Kargo to handle promotion. It models the version that moves between environments and records each move as its own entry: which version, which environment it went to, who or what triggered it, and when. The promotion also lands as a commit in Git, so the record of promoting something sits in the same history as the changes to the configuration. That leaves one timeline to read, with no separate deployment log for someone to reconcile.
That matters for the fifth question, whether production matches what was approved. A version reaches production only after it passes through the earlier stages, and the records of it doing so are generated by the process instead of typed up by a person.
Change management starts with a work item #
A work item is a written request for a piece of work, such as “upgrade the database” or “add a login page”. Tools like Jira track them, and some call them tickets or issues. The work item holds the intent: what was asked for, why, what the risk is and who accepted it.
Modern change management can run on the work item alone. The engineer picks up the work item, makes the change in Git and names the work item in the commit and the pull request. The two point at each other.
That link lets anyone check intent against reality. Read the work item to see what was supposed to change, then open the linked commits to see what did change. If the code does what the work item asked for, the evidence holds. If it does more or less, the gap is visible in the code and needs no memory or trust.
This is where change management is heading. A separate change record next to the work is duplicate administration, and engineers fill it in last and from memory. When the work item is the record, engineers spend their time engineering, and compliance checks the intent in the work item against the change in the code.
What all of this saves you #
The reason I like this setup is efficiency. In a ticket-driven process the audit trail is a job. Someone gathers the work items, matches them to deployments, chases missing approvals and explains the gaps. That work grows with every change and lands in the weeks before an audit.
With signed commits, required reviews and recorded promotions, nobody gathers evidence manually. It comes as a by-product of what you already do. The trail exists because the deployment happened, so when an auditor asks for every production change in a quarter, the answer is a query against a repository. The same setup is faster for the engineers too. They make the change, and the record comes with it.
That’s the real argument for GitOps as an audit trail. The evidence stops being separate work.
Where it still breaks #
Two cases put holes in all of this, but are fixable.
Emergency changes #
At some point production is on fire and waiting for a review is the wrong call. You need a way to act that is fast and still leaves a record. My rule is that an emergency path is allowed but never invisible, and it has four parts.
First, the platform records every request made to it: who, what, when and against which system. I ship that audit log to a dedicated log facility, kept apart from the systems it describes, so an emergency change made directly on the infrastructure is logged even though it didn’t go through Git. Second, before making the change, I pause the GitOps software for the affected application. The pause is itself an action that gets logged. Without it, the software would notice the difference and revert my fix within minutes.
Third, I make the change and commit the same change to Git before I resume. The repository has to describe the state the systems are now in. Fourth, I resume the software. Because Git now matches reality, nothing reverts. If I missed something, the software flags the leftover difference and I fix the repository.
The order matters. Commit first, resume second. Resuming while the repository still describes the old state undoes the emergency fix, which brings you back to square one. What you end up with is a trail with a small gap: a break-glass action, a pause, a commit and a resume, all timestamped. The review happens after the fact, but it still happens, and it is still a change in a repository.
Drift #
Drift is any difference between what the repository says should be running and what is actually running. A manual edit, a system that changed something on its own or a half-applied change can all cause it. Drift matters for audit because it breaks the fifth question. Once systems have drifted, the approved change and the real state have separated, and the history no longer tells the truth.
GitOps software compares the running systems with the repository on a schedule and can revert differences back to what Git says. That turns drift from a silent audit failure into a detectable, self-correcting event. A revert is itself evidence, because it shows the system noticed the difference and recovered. The strict access rules from Step 3 mean drift should be rare, and anything that does appear is either an emergency change waiting for its commit or a sign something is wrong.
Drift detection has a limit. The software only sees the things it manages, so anything outside its scope can drift without anyone noticing. Drift detection tells you the state matches Git for the things Git owns. It says nothing about the rest, so keep the boundary of what Git owns explicit, or even better, manage your entire infrastructure using GitOps.
What this doesn’t cover #
A Git history is evidence of what changed and who approved it. It says nothing about whether the change was a good idea. Risk assessment, business sign-off and the decision to accept a known risk all live in the work item, the meeting or the pull request description, and Git doesn’t replace them. Nor does it say anything about the people. Signing proves a key was used. Your onboarding and offboarding process has to make sure the right people hold the right keys.
Wrapping up #
Signed commits, enforced reviews, closed access and recorded promotions turn a change history into evidence that no ticket system can match, because the record and the change are the same thing. Emergency changes and drift stay visible with a logged break-glass path and software that reverts what Git doesn’t describe.
Engineers lose hours to change management, and nobody enjoys that work. With this setup an engineer picks up a work item, makes the change and moves on, and the evidence collects itself. Compliance gets proof it can trust because the evidence is a by-product of the change itself, and anyone can check it against the code.