TL;DR Talos and NixOS boot from a file that says what the machine is. That file is what an ISO 27001, SOC 2 or NIS2 auditor asks for, so the evidence collection shrinks to a git log, a diff with no output and an inventory dump. Below is the mapping from config to control, the scripts, and the part that stays hard work.
An ISO 27001 audit starts weeks before the auditor shows up, with someone collecting screenshots. Configuration panels, firewall rules, a dashboard that shows which patches went out. Then the Word documents that describe what the systems are supposed to do. Then the change tickets, dug out of the ticketing system, each one pointing at a “server maintenance” nobody can reconstruct six months later. Everyone treats this as the price of being in business. I did too, for years.
Then I’d been running Talos and NixOS for a while, and one of the auditor’s questions started to sound different. The whole job is one question, asked twenty ways. Prove the system is configured the way you claim. On a declarative system that has a short answer. Here’s the file it boots from.
That’s what this post is about. I picked these systems for sovereignty and for resilience, and the properties that made me pick them, immutable images, reproducible builds and a Git history I can trust, are the properties an auditor is fishing for. I didn’t plan that. It was there when I looked.
What auditors ask
Strip the framework-specific vocabulary and ISO 27001, SOC 2, NIS2 and PCI-DSS circle the same four questions.
- What is your system configuration? Asset management, configuration management.
- How do you control changes? Change management, access control.
- Can you prove it? Audit trails, evidence.
- Is it consistent? No drift, no undocumented changes.
In a traditional shop every one of these gets answered with paper. A Word document that describes what should be configured. A screenshot that proves it looked that way on one specific Tuesday. A change ticket that points at “server updates” which may or may not have happened the way the ticket says.
None of those documents is wrong on the day it’s written. The document says automatic updates are on, and they were, until the reorg. The screenshot was accurate when it was taken and says nothing about today. What’s in the evidence folder is a memory of the system, and nobody checks the memory against the machine until the next audit.
When the config is the documentation
Run Talos or NixOS and there’s no separate document to drift, because the configuration is the system. The machine refuses to boot in any state other than the one you declared, so the file and the running node can’t quietly disagree.
Talos
# This isn't documentation about the system.
# This IS the system.
machine:
type: controlplane
network:
hostname: prod-control-01
install:
disk: /dev/sda
image: ghcr.io/siderolabs/installer:v1.6.0
sysctls:
net.ipv4.ip_forward: "1"
kubelet:
extraArgs:
rotate-server-certificates: "true"
cluster:
clusterName: production
apiServer:
admissionControl:
- name: PodSecurity
configuration:
defaults:
enforce: "restricted"
When the auditor asks how the Kubernetes nodes are configured, you hand them this. Not an approximation, and not last quarter’s snapshot. The exact state, and the node is enforcing it right now.
NixOS
{
# Security hardening - this is both config AND documentation
security.sudo.wheelNeedsPassword = true;
security.auditd.enable = true;
security.audit.rules = [
"-w /etc/passwd -p wa -k identity"
"-w /etc/group -p wa -k identity"
];
# Firewall configuration
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 443 6443 ];
logRefusedConnections = true;
};
# Automatic security updates
system.autoUpgrade = {
enable = true;
allowReboot = false;
dates = "04:00";
};
}
Every control sits in code, and the auditor knows it’s live because NixOS won’t build a generation that doesn’t match it. There’s no “yes it’s configured, but we disabled it temporarily and forgot”. The build includes the control or the build fails.
Mapping declared state to the controls
“Trust me, it’s all in Git” doesn’t pass an audit on its own. Somebody has to connect each piece of config to the control it satisfies, and that somebody is you. Here’s how I do it for four controls I get asked about.
ISO 27001: A.12.1.2 - Change Management
The requirement says changes to systems shall be controlled. The traditional answer is change tickets, an approval workflow and documentation written by hand.
On a declarative system every change is a Git commit, a pull request needs a review before it merges, and merging to main is what deploys. The Git history is the change log.
# Your change management evidence
git log --oneline --since="2025-01-01" -- infrastructure/
2d8d00e Add NixOS vs Talos comparison
e49f5bd Add Talos Linux blog post
defb991 Switch image generation to DALL-E
...
Every commit carries who made it, when, what changed, who approved it in the PR review, and why in the message. A ticketing system gives you a worse version of the same record, and it’s decoupled from the change itself.
ISO 27001: A.12.6.1 - Management of Technical Vulnerabilities
The requirement says information about technical vulnerabilities shall be obtained and appropriate measures taken. With Talos the OS version is one line:
machine:
install:
image: ghcr.io/siderolabs/installer:v1.7.0 # Specific version
Updating means changing that line and deploying. The Git history shows which version ran on any date, and if a new version turns out to carry a vulnerability, the rollback is a revert.
NIS2: Article 21 - Cybersecurity Risk Management
The requirement asks for appropriate technical and organisational measures to manage risk. Here’s what three of those measures look like in NixOS, network segmentation, encryption at rest and access control:
{
# Network segmentation
networking.vlans = {
management = { id = 10; interface = "eth0"; };
production = { id = 20; interface = "eth0"; };
};
# Encryption at rest
boot.initrd.luks.devices."encrypted".device = "/dev/sda2";
# Access control
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
};
}
Each control is visible, and the build enforces it.
SOC 2: CC6.1 - Logical Access Controls
The requirement says the entity implements logical access security software, infrastructure and architectures. This is the one I enjoy most, because the Talos answer to logical access control is to not have a login. There’s no SSH. All management goes through the API, every API call is authenticated with mTLS , and what you may do once you’re in is Kubernetes RBAC.
# Talos machine config
machine:
features:
rbac: true # Kubernetes RBAC enabled
# No SSH configuration because SSH doesn't exist
The auditor can check on the node itself that there’s no SSH daemon to break into. There’s nothing else to inspect, and no policy document that says please don’t.
The audit that runs itself
Because everything lives in code, the evidence collection that used to eat weeks is three scripts on a schedule.
Configuration drift report
#!/bin/bash
# For Talos: compare declared vs actual
for node in $(talosctl get members -o json | jq -r '.[] | .hostname'); do
echo "=== $node ==="
talosctl get machineconfig -n $node -o yaml > /tmp/actual.yaml
diff infrastructure/talos/$node.yaml /tmp/actual.yaml
done
No output means no drift, and the empty diff goes into the evidence folder as it is.
NixOS system closure
# Generate a complete list of all software and versions
nix-store -q --requisites /run/current-system | \
xargs -I {} nix-store -q --binding name {} 2>/dev/null | \
sort -u > system-inventory.txt
One command and you’ve got the complete software inventory, every package and library with the version that’s running. Auditors who are used to chasing that by hand light up when you hand them a file.
Git-based audit trail
# Generate change report for audit period
git log --pretty=format:"%h|%an|%ad|%s" \
--date=short \
--since="2025-01-01" \
--until="2025-12-31" \
-- infrastructure/ > annual-changes.csv
Import it into Excel. Done.
Compliant between audits
There’s a step further. Pair the declarative systems with GitOps and the cluster corrects itself back to the declared state all year, whether or not anyone is coming to look.
# ArgoCD Application for infrastructure
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure
spec:
source:
repoURL: https://github.com/company/infrastructure
path: kubernetes/
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: true
selfHeal: true # Automatically fix drift
Someone patches a node by hand during an incident? ArgoCD reverts it on the next sync. So “were you compliant on March 14th” has an answer in the Git log and the sync history for March 14th, and I don’t have to remember.
Doing the boring parts well
None of this works on its own. Declarative infrastructure makes good compliance evidence possible, and then four habits decide whether you get it.
1. Write meaningful commit messages
# Bad
git commit -m "Update config"
# Good
git commit -m "Enable audit logging per ISO 27001 A.12.4.1
- Added auditd configuration for file access monitoring
- Configured log retention for 90 days
- Addresses finding from Q3 security review
Ticket: SEC-1234"
Your Git history is going to be read by an auditor someday. Write it for that reader.
2. Tag compliance-related config
{
# ISO 27001 A.12.4.1 - Event Logging
security.auditd.enable = true;
# ISO 27001 A.13.1.1 - Network Controls
networking.firewall.enable = true;
# NIS2 Art.21(2)(d) - Supply Chain Security
nix.settings.require-sigs = true;
}
A comment that links a line of config to a control turns the audit from a scavenger hunt into a grep.
3. Pin every version
# Talos machine config
machine:
install:
image: ghcr.io/siderolabs/installer:v1.6.0 # Not :latest
Auditors want to know what ran on a specific date. Pinned versions plus Git history answer that for free. :latest throws the answer away.
4. Automate evidence collection
#!/bin/bash
# Quarterly compliance report generator
REPORT_DIR="compliance-reports/$(date +%Y-Q%q)"
mkdir -p $REPORT_DIR
# System inventory
nix-store -q --requisites /run/current-system > $REPORT_DIR/inventory.txt
# Configuration snapshots
cp -r /etc/nixos $REPORT_DIR/nixos-config/
# Change log
git log --since="3 months ago" -- infrastructure/ > $REPORT_DIR/changes.txt
# Drift check
./scripts/check-drift.sh > $REPORT_DIR/drift-report.txt
echo "Report generated in $REPORT_DIR"
Run it every quarter, archive the output, and the folder is ready when the auditor shows up.
Be honest about the cost
I’m not going to pretend this is free. Talos and NixOS both have a steeper learning curve than installing Ubuntu and clicking through a hardening guide. NixOS in particular will fight you for the first month while you learn the language. You trade a forgiving setup for a strict one, and the strictness lands on day one.
Then there’s the mapping work. The system gives you enforced, version-controlled state, and somebody still has to sit down and connect each piece of config to the control it satisfies. The auditor won’t do that translation for you, and neither will the tooling. On day one it can feel like you’ve added a compliance task rather than removed one.
The difference is when you pay. This cost is paid once, and every audit after that reads the same repository. The screenshot-and-Word-document approach pays every cycle, and the bill grows with the estate.
Try it on one cluster
You don’t have to convert the whole estate to find out whether this holds. Pick one cluster. Stand it up on Talos, put the config in Git, point ArgoCD at it with selfHeal on. Run it for a quarter and see what your evidence folder looks like when the audit comes around, next to the team that’s still building a slide deck out of screenshots.
So the first paragraph of this post is still true. The screenshots, the Word documents and the tickets are what an audit asks for, and on a traditional setup they’re the only way to answer. What I’d missed for years is that they’re all stand-ins for one file, the one that says what the machine is. Talos and NixOS boot from that file. Hand it over, and what’s left is the mapping work.
