Every B2B SaaS company hits the same wall. You close a few small customers on reputation and a demo. Then a mid-market prospect sends over a vendor security questionnaire — 300 questions, starting with "Provide your most recent SOC 2 Type II report." You do not have one. You also do not have ISO 27001 certification. What you have is a production environment that probably does most of the right things but cannot prove it to anyone who was not there when you built it.
This is where most companies panic and start shopping for compliance consultants. They spend three to six months retro-fitting controls onto infrastructure that was never designed for auditability, scrambling to produce evidence that does not exist, and writing policies that describe an idealised version of their operations rather than the reality. The audit passes — barely — and then the controls start drifting because nobody built them into the infrastructure in the first place.
There is a better approach. If you build your hosting infrastructure with audit readiness as a design principle — not a bolt-on — the certification process becomes documentation of what already exists. The controls are automated. The evidence collects itself. The policies describe actual operational procedures because the procedures were designed around the policies from day one.
This guide covers how to architect that infrastructure on Swiss servers — where managed Swiss infrastructure under the FADP provides jurisdictional advantages that simplify compliance narratives for both SOC 2 and ISO 27001 assessments, particularly when your customers operate under GDPR or Swiss financial regulation.
Before building anything, understand what these frameworks demand at a structural level. They overlap significantly, but their philosophies and scoping models differ in ways that affect your architecture decisions.
SOC 2 Type II is a US-centric attestation framework developed by the AICPA. It evaluates your controls against five Trust Services Criteria: Security (mandatory), Availability, Processing Integrity, Confidentiality, and Privacy. Type II means the auditor evaluates not just the design of your controls but their operating effectiveness over a period — typically 6 to 12 months. The auditor is a licensed CPA firm. The output is an attestation report that enterprise prospects require before signing.
ISO 27001 is an international standard for Information Security Management Systems (ISMS). It is certification-based rather than attestation-based — a certified registrar audits your ISMS against the standard's requirements and Annex A controls. ISO 27001 is broader in scope (it covers the management system itself, not just technical controls) and is more recognised outside the US, particularly in Europe and Asia-Pacific. The certification is valid for three years with annual surveillance audits.
Where they overlap: Access control, change management, incident management, risk assessment, encryption, logging, vendor management, business continuity. If you implement controls that satisfy both frameworks, the marginal effort for the second certification is mostly documentation mapping rather than new technical work.
Where they diverge: SOC 2 is more prescriptive about evidence and testing of controls over time. ISO 27001 is more prescriptive about the management system — risk assessment methodology, management review meetings, internal audit programmes, continual improvement processes. SOC 2 does not require a formal ISMS; ISO 27001 does not require the Trust Services Criteria structure.
The practical takeaway: build your infrastructure controls to satisfy the union of both frameworks, then map the evidence to each framework separately during the audit process. This is cheaper and more robust than treating them as separate projects.
An audit-ready infrastructure is not fundamentally different from a well-run infrastructure. The difference is that every security-relevant decision is documented, every control is measurable, and every exception is tracked. Here is the architecture layer by layer.
Network segmentation and access control
Both SOC 2 (CC6.1 — Logical and Physical Access Controls) and ISO 27001 (A.13.1 — Network Security Management) require documented network boundaries with controlled access between zones. The typical three-tier model — DMZ, application tier, data tier — is the minimum. For fintech workloads, add a management tier that is network-isolated from all production traffic.
# Network segmentation with nftables
# /etc/nftables.conf — Production application server
table inet filter {
# Define network zones
set dmz_nets {
type ipv4_addr
flags interval
elements = { 10.10.0.0/24 }
}
set app_nets {
type ipv4_addr
flags interval
elements = { 10.20.0.0/24 }
}
set data_nets {
type ipv4_addr
flags interval
elements = { 10.30.0.0/24 }
}
set mgmt_nets {
type ipv4_addr
flags interval
elements = { 10.40.0.0/24 }
}
chain input {
type filter hook input priority 0; policy drop;
# Allow established connections
ct state established,related accept
# Allow loopback
iif lo accept
# Management access — SSH only from management network
ip saddr @mgmt_nets tcp dport 22 accept
# Application traffic — HTTPS from DMZ load balancers only
ip saddr @dmz_nets tcp dport { 8080, 8443 } accept
# Health checks from monitoring (management network)
ip saddr @mgmt_nets tcp dport 9090 accept
# Log and drop everything else
log prefix "nft-drop: " counter drop
}
chain output {
type filter hook output priority 0; policy drop;
# Allow established connections
ct state established,related accept
# Database access — only to data tier
ip daddr @data_nets tcp dport { 5432, 6379 } accept
# Vault access — only to management tier
ip daddr @mgmt_nets tcp dport 8200 accept
# DNS resolution
tcp dport 53 accept
udp dport 53 accept
# NTP (required for audit log accuracy)
udp dport 123 accept
# Log collection to central syslog
ip daddr @mgmt_nets tcp dport 514 accept
# Allow HTTPS for external API calls (payment processors, etc.)
tcp dport 443 accept
# Drop and log everything else
log prefix "nft-out-drop: " counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
# No forwarding on application servers
}
}
Every rule has an implicit purpose that maps to a control. SSH only from the management network satisfies access control requirements. Database traffic only to the data tier satisfies network segmentation. The drop-and-log default policy provides evidence that unauthorized traffic is blocked and recorded. The NTP allowance is specifically there because audit log timestamp accuracy is a SOC 2 requirement (CC7.1) — if your server clocks drift, your audit evidence becomes unreliable.
Identity and access management
SOC 2 CC6.1-CC6.3 and ISO 27001 A.9 both require that access is granted based on the principle of least privilege, regularly reviewed, and promptly revoked when no longer needed. The implementation has three components: authentication, authorization, and access review.
# SSH hardening for audit compliance
# /etc/ssh/sshd_config — every production server
# Authentication: key-based only, no passwords
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
# Restrict to named users (no group-based SSH access)
AllowUsers deploy_svc audit_svc oncall_eng_01 oncall_eng_02
# Session controls
ClientAliveInterval 300
ClientAliveCountMax 2
MaxSessions 3
MaxAuthTries 3
LoginGraceTime 30
# Logging — every authentication event
LogLevel VERBOSE
SyslogFacility AUTH
# Disable everything unnecessary
X11Forwarding no
AllowTcpForwarding no
AllowStreamLocalForwarding no
PermitTunnel no
PermitUserEnvironment no
PermitRootLogin no
# Restrict key algorithms to current standards
PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com
HostKeyAlgorithms ssh-ed25519
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com
# Automated access review script
# /usr/local/bin/access-review.sh
# Runs weekly via cron; generates evidence for quarterly access reviews
#!/bin/bash
set -euo pipefail
REPORT_DIR="/var/log/compliance/access-reviews"
DATE=$(date +%Y-%m-%d)
REPORT="${REPORT_DIR}/access-review-${DATE}.json"
mkdir -p "${REPORT_DIR}"
# Collect current access state
echo "{" > "${REPORT}"
echo " \"review_date\": \"${DATE}\"," >> "${REPORT}"
echo " \"hostname\": \"$(hostname)\"," >> "${REPORT}"
# SSH authorized users
echo " \"ssh_users\": [" >> "${REPORT}"
grep "^AllowUsers" /etc/ssh/sshd_config | \
sed 's/AllowUsers //' | tr ' ' '\n' | \
awk '{printf " {\"user\": \"%s\", \"method\": \"ssh_key\"}", $1}' | \
paste -sd',\n' >> "${REPORT}"
echo "" >> "${REPORT}"
echo " ]," >> "${REPORT}"
# Users with sudo access
echo " \"sudo_users\": [" >> "${REPORT}"
grep -r "^[^#]" /etc/sudoers.d/ 2>/dev/null | \
awk -F: '{print $2}' | \
awk '{printf " {\"user\": \"%s\", \"privileges\": \"%s\"}\n", $1, $0}' | \
paste -sd',\n' >> "${REPORT}"
echo "" >> "${REPORT}"
echo " ]," >> "${REPORT}"
# Active database roles (via Vault — dynamic credentials)
echo " \"vault_roles\": [" >> "${REPORT}"
vault list -format=json database/roles 2>/dev/null | \
jq -r '.[] | " {\"role\": \"" + . + "\"}"' | \
paste -sd',\n' >> "${REPORT}"
echo "" >> "${REPORT}"
echo " ]," >> "${REPORT}"
# Last login times for all users
echo " \"last_logins\": [" >> "${REPORT}"
lastlog -b 90 2>/dev/null | tail -n +2 | \
awk '$0 !~ /Never logged in/ {printf " {\"user\": \"%s\", \"last_login\": \"%s %s %s %s\"}\n", $1, $4, $5, $6, $7}' | \
paste -sd',\n' >> "${REPORT}"
echo "" >> "${REPORT}"
echo " ]," >> "${REPORT}"
# Accounts with no login in 90+ days (flag for review)
echo " \"stale_accounts\": [" >> "${REPORT}"
lastlog -b 0 -t 90 2>/dev/null | tail -n +2 | \
grep "Never logged in" | \
awk '{printf " {\"user\": \"%s\", \"status\": \"never_logged_in\"}\n", $1}' | \
paste -sd',\n' >> "${REPORT}"
echo "" >> "${REPORT}"
echo " ]" >> "${REPORT}"
echo "}" >> "${REPORT}"
# Verify JSON validity
if jq empty "${REPORT}" 2>/dev/null; then
echo "Access review generated: ${REPORT}"
else
echo "WARNING: Generated report has JSON errors"
fi
The access review script runs weekly and generates machine-readable evidence. When the auditor asks "show me your access reviews for Q3," you hand them 13 weekly reports that document every user, their access level, and when they last authenticated. Stale accounts are automatically flagged. This is not a spreadsheet someone fills out once a quarter from memory — it is automated evidence that reflects the actual state of your systems.
SOC 2 CC8.1 and ISO 27001 A.12.1.2 require that changes to production systems are authorized, tested, and documented. For infrastructure, this means every configuration change, deployment, and system modification must be traceable to an authorized request.
# Git-based configuration management
# All server configs tracked in a private repository
# Changes require pull request + review before merge
# .github/workflows/config-deploy.yml
name: Configuration Deployment
on:
push:
branches: [main]
paths:
- 'servers/**'
- 'firewall/**'
- 'monitoring/**'
jobs:
validate:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Validate nftables syntax
run: |
for f in firewall/*.conf; do
nft -c -f "$f" || exit 1
done
- name: Validate SSH configs
run: |
for f in servers/*/sshd_config; do
sshd -t -f "$f" || exit 1
done
- name: Dry-run Ansible playbook
run: ansible-playbook site.yml --check --diff
deploy:
needs: validate
runs-on: self-hosted
environment: production # Requires manual approval for prod
steps:
- uses: actions/checkout@v4
- name: Deploy configuration changes
run: |
ansible-playbook site.yml --diff 2>&1 | \
tee /var/log/compliance/deployments/deploy-$(date +%Y%m%d-%H%M%S).log
- name: Record change in audit log
run: |
echo "{
\"timestamp\": \"$(date -Iseconds)\",
\"change_id\": \"${{ github.sha }}\",
\"author\": \"${{ github.actor }}\",
\"approver\": \"${{ github.event.head_commit.committer.name }}\",
\"description\": \"${{ github.event.head_commit.message }}\",
\"files_changed\": $(echo '${{ toJSON(github.event.commits.*.modified) }}'),
\"environment\": \"production\"
}" >> /var/log/compliance/change-log.jsonl
The pattern is straightforward: configuration lives in Git (full history, authorship, diffs), changes require pull request review (separation of duties), deployment requires environment approval (authorization gate), and every deployment is logged with the change ID, author, approver, and description. When the auditor asks "walk me through your change management process," you show them the Git history, the PR reviews, and the deployment logs — all linked by commit hash.
This is not theoretical. This is how the infrastructure should actually work every day. If someone SSH-es into a production server and modifies a config file manually, the next Ansible run will either revert the change (drift detection) or fail with a diff that shows unauthorized modifications. Both outcomes create evidence.
The most expensive part of compliance is not implementing controls — it is proving they worked consistently over the audit period. SOC 2 Type II specifically evaluates operating effectiveness: did this control work every day for the past 12 months, or just the day before the audit?
Automated evidence collection eliminates the scramble. Instead of reconstructing what happened from fragmented logs during audit prep, your monitoring infrastructure continuously generates the evidence the auditor needs.
# Compliance evidence collection daemon
# /usr/local/bin/compliance-evidence-collector.py
# Runs hourly; collects and stores compliance-relevant system state
#!/usr/bin/env python3
"""
Automated compliance evidence collector.
Generates structured evidence for SOC 2 and ISO 27001 controls.
"""
import json
import subprocess
import os
import hashlib
from datetime import datetime, timezone
from pathlib import Path
EVIDENCE_DIR = Path("/var/log/compliance/evidence")
EVIDENCE_DIR.mkdir(parents=True, exist_ok=True)
def collect_evidence(control_id: str, description: str,
collector_fn) -> dict:
"""Collect and store evidence for a specific control."""
timestamp = datetime.now(timezone.utc)
evidence = {
"control_id": control_id,
"description": description,
"collected_at": timestamp.isoformat(),
"hostname": os.uname().nodename,
"collector_version": "1.2.0",
}
try:
result = collector_fn()
evidence["status"] = "pass" if result.get("compliant") else "fail"
evidence["data"] = result
except Exception as e:
evidence["status"] = "error"
evidence["error"] = str(e)
# Write evidence with integrity hash
evidence_json = json.dumps(evidence, sort_keys=True)
evidence["integrity_hash"] = hashlib.sha256(
evidence_json.encode()
).hexdigest()
# Store by date and control
date_dir = EVIDENCE_DIR / timestamp.strftime("%Y-%m")
date_dir.mkdir(exist_ok=True)
filename = f"{control_id}_{timestamp.strftime('%Y%m%d_%H%M%S')}.json"
filepath = date_dir / filename
with open(filepath, 'w') as f:
json.dump(evidence, f, indent=2)
return evidence
def check_encryption_at_rest() -> dict:
"""SOC 2 CC6.7 / ISO 27001 A.10.1 — Encryption at rest."""
result = {"compliant": True, "findings": []}
# Check LUKS disk encryption
luks_output = subprocess.run(
["lsblk", "-o", "NAME,FSTYPE,MOUNTPOINT", "-J"],
capture_output=True, text=True
)
block_devices = json.loads(luks_output.stdout)
for device in block_devices.get("blockdevices", []):
if device.get("mountpoint") in ["/", "/var", "/opt"]:
if "crypt" not in str(device.get("fstype", "")):
# Check if parent is LUKS
has_luks = any(
c.get("fstype") == "crypto_LUKS"
for c in device.get("children", [])
)
if not has_luks:
result["compliant"] = False
result["findings"].append(
f"{device['name']} mounted at "
f"{device['mountpoint']} — not encrypted"
)
# Check database encryption configuration
try:
pg_output = subprocess.run(
["sudo", "-u", "postgres", "psql", "-t", "-c",
"SHOW ssl;"],
capture_output=True, text=True, timeout=5
)
if "on" not in pg_output.stdout:
result["compliant"] = False
result["findings"].append("PostgreSQL SSL not enabled")
except (subprocess.TimeoutExpired, FileNotFoundError):
pass # PostgreSQL not on this host
return result
def check_password_policy() -> dict:
"""SOC 2 CC6.1 / ISO 27001 A.9.4 — Authentication controls."""
result = {"compliant": True, "findings": []}
# Verify password authentication is disabled for SSH
sshd_output = subprocess.run(
["sshd", "-T"],
capture_output=True, text=True
)
config = sshd_output.stdout.lower()
if "passwordauthentication yes" in config:
result["compliant"] = False
result["findings"].append("SSH password authentication enabled")
if "permitrootlogin yes" in config or \
"permitrootlogin without-password" in config:
result["compliant"] = False
result["findings"].append("SSH root login permitted")
# Check for accounts with empty passwords
with open("/etc/shadow", "r") as f:
for line in f:
parts = line.strip().split(":")
if len(parts) >= 2 and parts[1] in ("", "!"):
if parts[0] not in ("sync", "shutdown", "halt",
"nobody", "nfsnobody"):
result["findings"].append(
f"Account {parts[0]} has no password set"
)
return result
def check_logging_integrity() -> dict:
"""SOC 2 CC7.2 / ISO 27001 A.12.4 — Log integrity."""
result = {"compliant": True, "findings": []}
# Verify rsyslog is running and forwarding to central log server
rsyslog_status = subprocess.run(
["systemctl", "is-active", "rsyslog"],
capture_output=True, text=True
)
if rsyslog_status.stdout.strip() != "active":
result["compliant"] = False
result["findings"].append("rsyslog not running")
# Check that audit logs exist and are recent
audit_log = Path("/var/log/audit/audit.log")
if audit_log.exists():
age_seconds = (
datetime.now().timestamp() - audit_log.stat().st_mtime
)
if age_seconds > 3600:
result["compliant"] = False
result["findings"].append(
f"Audit log stale — last modified "
f"{int(age_seconds/3600)}h ago"
)
else:
result["compliant"] = False
result["findings"].append("Audit log file does not exist")
# Verify auditd rules are loaded
rules_output = subprocess.run(
["auditctl", "-l"],
capture_output=True, text=True
)
if "No rules" in rules_output.stdout:
result["compliant"] = False
result["findings"].append("No audit rules configured")
return result
def check_patch_status() -> dict:
"""SOC 2 CC7.1 / ISO 27001 A.12.6 — Vulnerability management."""
result = {"compliant": True, "findings": []}
# Check for available security updates
updates_output = subprocess.run(
["apt", "list", "--upgradable"],
capture_output=True, text=True, env={**os.environ, "LANG": "C"}
)
security_updates = [
line for line in updates_output.stdout.split("\n")
if "security" in line.lower()
]
result["pending_security_updates"] = len(security_updates)
result["update_details"] = security_updates[:10] # First 10
if len(security_updates) > 0:
result["findings"].append(
f"{len(security_updates)} security updates pending"
)
# More than 5 outstanding security patches is non-compliant
if len(security_updates) > 5:
result["compliant"] = False
# Check kernel version and uptime
kernel = subprocess.run(
["uname", "-r"], capture_output=True, text=True
)
result["kernel_version"] = kernel.stdout.strip()
uptime = subprocess.run(
["uptime", "-s"], capture_output=True, text=True
)
result["last_reboot"] = uptime.stdout.strip()
return result
def check_backup_status() -> dict:
"""SOC 2 A1.2 / ISO 27001 A.12.3 — Backup verification."""
result = {"compliant": True, "findings": []}
backup_log = Path("/var/log/backup/last-backup.json")
if backup_log.exists():
with open(backup_log) as f:
last_backup = json.load(f)
last_time = datetime.fromisoformat(last_backup["completed_at"])
age_hours = (
datetime.now(timezone.utc) - last_time
).total_seconds() / 3600
result["last_backup_age_hours"] = round(age_hours, 1)
result["last_backup_status"] = last_backup.get("status")
result["last_backup_size_mb"] = last_backup.get("size_mb")
if age_hours > 48:
result["compliant"] = False
result["findings"].append(
f"Last backup is {round(age_hours)}h old (max 48h)"
)
if last_backup.get("status") != "success":
result["compliant"] = False
result["findings"].append(
f"Last backup status: {last_backup.get('status')}"
)
else:
result["compliant"] = False
result["findings"].append("No backup log found")
return result
# Run all evidence collectors
collectors = {
"CC6.7-A10.1": ("Encryption at rest", check_encryption_at_rest),
"CC6.1-A9.4": ("Authentication controls", check_password_policy),
"CC7.2-A12.4": ("Log integrity", check_logging_integrity),
"CC7.1-A12.6": ("Vulnerability management", check_patch_status),
"A1.2-A12.3": ("Backup verification", check_backup_status),
}
results = {}
for control_id, (description, collector) in collectors.items():
evidence = collect_evidence(control_id, description, collector)
results[control_id] = evidence["status"]
if evidence["status"] != "pass":
print(f" FINDING: {control_id} — {description}: "
f"{evidence.get('data', {}).get('findings', [])}")
# Summary
pass_count = sum(1 for s in results.values() if s == "pass")
total = len(results)
print(f"\nCompliance evidence collected: "
f"{pass_count}/{total} controls passing")
This collector runs hourly and generates timestamped evidence files organized by month. Over a 12-month SOC 2 audit period, you accumulate approximately 8,760 evidence snapshots per server per control. The auditor does not need to sample randomly and hope — they can see the continuous state of every control across the entire audit period. That is the difference between "we think this control was effective" and "here are 8,760 data points proving it was effective."
Both SOC 2 (CC7.3, CC7.4, CC7.5) and ISO 27001 (A.16) require documented incident management procedures. The auditor will ask for your incident response plan, evidence that it has been tested, and records of actual incidents including their resolution and post-mortem analysis.
The infrastructure component of incident management is detection and alerting. Your monitoring system needs to detect security-relevant events and route them to the right people with enough context to act.
# Security incident detection rules
# /etc/prometheus/rules/security-incidents.yml
groups:
- name: security_incidents
interval: 15s
rules:
# Brute force detection — more than 20 failed SSH logins in 5 min
- alert: SSHBruteForce
expr: |
increase(node_failed_logins_total[5m]) > 20
labels:
severity: high
incident_type: brute_force
soc2_control: CC6.1
iso27001_control: A.9.4
annotations:
summary: "SSH brute force detected on {{ $labels.instance }}"
runbook_url: "https://wiki.internal/runbooks/ssh-brute-force"
evidence: "Failed login count: {{ $value }} in 5 minutes"
# Unauthorized configuration change detected
- alert: ConfigDrift
expr: |
node_config_drift_detected == 1
labels:
severity: critical
incident_type: unauthorized_change
soc2_control: CC8.1
iso27001_control: A.12.1.2
annotations:
summary: "Configuration drift detected on {{ $labels.instance }}"
runbook_url: "https://wiki.internal/runbooks/config-drift"
# Privilege escalation attempt
- alert: PrivilegeEscalation
expr: |
increase(node_sudo_failures_total[5m]) > 5
labels:
severity: critical
incident_type: privilege_escalation
soc2_control: CC6.3
iso27001_control: A.9.2
annotations:
summary: "Privilege escalation attempts on {{ $labels.instance }}"
# Unusual outbound data transfer (potential exfiltration)
- alert: UnusualOutboundTraffic
expr: |
rate(node_network_transmit_bytes_total{device!="lo"}[15m])
> 50 * 1024 * 1024 # 50 MB/s sustained
unless on(instance)
node_network_transmit_bytes_baseline
for: 10m
labels:
severity: high
incident_type: data_exfiltration_risk
soc2_control: CC6.7
iso27001_control: A.13.2
annotations:
summary: "Unusual outbound traffic from {{ $labels.instance }}"
description: "{{ $value | humanize }}B/s sustained outbound"
# File integrity violation
- alert: FileIntegrityViolation
expr: |
aide_violations_total > 0
labels:
severity: critical
incident_type: file_tampering
soc2_control: CC7.2
iso27001_control: A.12.4
annotations:
summary: "File integrity violation on {{ $labels.instance }}"
description: "{{ $value }} files modified outside change management"
Notice the control ID labels on every alert rule. When an incident fires and gets resolved, the post-mortem documentation automatically references the relevant SOC 2 and ISO 27001 controls. The auditor can search your incident database by control ID and see every event related to that control — detection, response, resolution, and root cause analysis.
ISO 27001 requires a formal risk assessment methodology — you must identify information security risks, assess their likelihood and impact, and select controls proportionate to the risk. SOC 2 does not formally require a risk assessment, but having one strengthens your Trust Services Criteria narrative and makes control selection defensible.
For infrastructure, the risk assessment should be anchored to concrete technical scenarios rather than abstract risk categories. Here is a practical framework:
• Asset identification: Production databases, customer PII stores, encryption keys, API credentials, application source code, CI/CD pipelines, monitoring infrastructure, backup systems, DNS configuration, TLS certificates
• Threat identification: External attack (DDoS, application exploitation, credential stuffing), insider threat (privileged access misuse, accidental exposure), supply chain compromise (dependency vulnerability, CI/CD pipeline injection), infrastructure failure (disk failure, network partition, data centre outage), regulatory action (data disclosure order, cross-border enforcement)
• Risk evaluation: For each asset-threat combination, assess likelihood (rare, unlikely, possible, likely, almost certain) and impact (negligible, minor, moderate, major, catastrophic). The product gives you a risk score that determines control priority
• Control selection: Map each material risk to specific controls from ISO 27001 Annex A and SOC 2 Trust Services Criteria. Document why each control was selected and what residual risk remains after implementation
The jurisdictional risk is where Swiss hosting becomes specifically relevant to the risk assessment. If your infrastructure is in a jurisdiction where foreign authorities can compel data disclosure without your knowledge (under the US CLOUD Act, for example), that is a risk that needs to be assessed and addressed. Managed Swiss infrastructure under the FADP mitigates this specific risk through the Swiss legal framework's requirements for judicial oversight of foreign data requests — a control that cannot be replicated technically but is enforced legally.
SOC 2 CC9.2 and ISO 27001 A.15 require that you assess and monitor the security of your vendors — anyone who processes, stores, or has access to data in your scope. For a SaaS company, this includes your hosting provider, DNS provider, CDN, payment processor, email service, monitoring tools, and any third-party APIs that touch customer data.
# Vendor risk assessment tracking
# /opt/compliance/vendor-registry.yaml
vendors:
- name: "SwissLayer (Infrastructure)"
category: "critical" # Hosts production workloads
data_access: "infrastructure-level"
jurisdiction: "Switzerland"
certifications:
- "ISO 27001"
contracts:
dpa_signed: true
dpa_date: "2026-01-15"
sla_uptime: "99.95%"
data_location: "Zurich, Switzerland"
subprocessors_disclosed: true
last_review: "2026-07-01"
next_review: "2027-01-01"
risk_rating: "low"
notes: "Swiss jurisdiction — FADP adequacy. No CLOUD Act exposure."
- name: "Payment Processor"
category: "critical"
data_access: "payment_data"
jurisdiction: "EU"
certifications:
- "PCI DSS Level 1"
- "SOC 2 Type II"
contracts:
dpa_signed: true
sla_uptime: "99.99%"
last_review: "2026-06-15"
next_review: "2026-12-15"
risk_rating: "medium" # Handles financial data directly
- name: "Email Service Provider"
category: "standard"
data_access: "email_addresses"
jurisdiction: "US"
certifications:
- "SOC 2 Type II"
contracts:
dpa_signed: true
standard_contractual_clauses: true # Required for US jurisdiction
last_review: "2026-05-01"
next_review: "2026-11-01"
risk_rating: "medium"
notes: "US jurisdiction — SCCs required. Monitor for adequacy changes."
The vendor registry is a living document — updated when contracts renew, when vendors provide updated certifications, or when jurisdictional risk changes. The auditor will ask to see your vendor assessment process, your criteria for risk rating, and evidence that you review vendors on a defined schedule. Having a structured registry with dates, ratings, and notes is orders of magnitude better than a folder of PDF contracts that nobody has reviewed since signing.
The single biggest time sink during audit preparation is not fixing controls — it is finding evidence that controls were working. The evidence architecture should be designed so that evidence is generated as a side effect of normal operations, stored in a tamper-evident format, and retrievable by control ID and date range.
# Evidence storage structure
# /var/log/compliance/
# ├── evidence/
# │ ├── 2026-01/
# │ │ ├── CC6.1-A9.4_20260115_120000.json # Access control
# │ │ ├── CC6.7-A10.1_20260115_120000.json # Encryption
# │ │ ├── CC7.1-A12.6_20260115_120000.json # Patching
# │ │ └── ...
# │ ├── 2026-02/
# │ └── ...
# ├── access-reviews/
# │ ├── access-review-2026-01-07.json
# │ ├── access-review-2026-01-14.json
# │ └── ...
# ├── change-log.jsonl # All deployments
# ├── incidents/
# │ ├── INC-2026-001.json
# │ ├── INC-2026-002.json
# │ └── ...
# ├── deployments/
# │ ├── deploy-20260115-143022.log
# │ └── ...
# └── risk-assessments/
# ├── risk-assessment-2026-Q1.pdf
# └── risk-assessment-2026-Q3.pdf
# Evidence retrieval script for audit preparation
#!/bin/bash
# /usr/local/bin/audit-evidence-export.sh
# Usage: audit-evidence-export.sh 2026-01-01 2026-12-31 CC6.1
START_DATE="$1"
END_DATE="$2"
CONTROL_ID="$3"
EXPORT_DIR="/tmp/audit-export-$(date +%Y%m%d)"
mkdir -p "${EXPORT_DIR}"
echo "Exporting evidence for ${CONTROL_ID} from ${START_DATE} to ${END_DATE}"
# Find and copy all evidence files matching the control and date range
find /var/log/compliance/evidence/ \
-name "${CONTROL_ID}*" \
-newer <(date -d "${START_DATE}" +%s) \
! -newer <(date -d "${END_DATE}" +%s) \
-exec cp {} "${EXPORT_DIR}/" \;
# Generate summary statistics
echo "Evidence summary:" > "${EXPORT_DIR}/SUMMARY.txt"
echo "Control: ${CONTROL_ID}" >> "${EXPORT_DIR}/SUMMARY.txt"
echo "Period: ${START_DATE} to ${END_DATE}" >> "${EXPORT_DIR}/SUMMARY.txt"
echo "Total evidence files: $(ls ${EXPORT_DIR}/*.json 2>/dev/null | wc -l)" \
>> "${EXPORT_DIR}/SUMMARY.txt"
# Count pass/fail over the period
PASS=$(grep -l '"status": "pass"' ${EXPORT_DIR}/*.json 2>/dev/null | wc -l)
FAIL=$(grep -l '"status": "fail"' ${EXPORT_DIR}/*.json 2>/dev/null | wc -l)
echo "Pass: ${PASS}" >> "${EXPORT_DIR}/SUMMARY.txt"
echo "Fail: ${FAIL}" >> "${EXPORT_DIR}/SUMMARY.txt"
echo "Compliance rate: $(echo "scale=2; ${PASS}*100/(${PASS}+${FAIL})" | bc)%" \
>> "${EXPORT_DIR}/SUMMARY.txt"
echo "Export complete: ${EXPORT_DIR}"
echo "$(ls ${EXPORT_DIR}/ | wc -l) files ready for auditor"
When the auditor requests evidence for CC6.1 (Access Control) for the audit period, you run one command and hand them a directory with thousands of timestamped evidence files, each documenting the exact state of your access controls at that point in time, plus a summary showing your compliance rate. Compare this to the company that spends two weeks manually compiling screenshots and writing narratives from memory. The infrastructure approach is faster, more accurate, and produces evidence that auditors actually trust because it was generated automatically rather than curated retroactively.
SOC 2 Availability criteria (A1.1, A1.2, A1.3) and ISO 27001 A.17 require documented business continuity plans with tested recovery procedures. For hosting infrastructure, this means backup verification, recovery time objectives (RTOs), recovery point objectives (RPOs), and evidence that you have actually tested recovery — not just documented it.
# Automated DR test — runs monthly
# /usr/local/bin/dr-test.sh
#!/bin/bash
set -euo pipefail
DR_LOG="/var/log/compliance/dr-tests"
DATE=$(date +%Y-%m-%d)
TEST_LOG="${DR_LOG}/dr-test-${DATE}.json"
mkdir -p "${DR_LOG}"
echo "{" > "${TEST_LOG}"
echo " \"test_date\": \"${DATE}\"," >> "${TEST_LOG}"
echo " \"test_type\": \"automated_monthly\"," >> "${TEST_LOG}"
echo " \"tests\": [" >> "${TEST_LOG}"
# Test 1: Backup integrity verification
echo "Testing backup integrity..."
BACKUP_FILE=$(ls -t /backup/daily/*.gpg | head -1)
CHECKSUM_FILE="${BACKUP_FILE}.sha256"
VERIFY_START=$(date +%s)
if sha256sum -c "${CHECKSUM_FILE}" 2>/dev/null; then
BACKUP_RESULT="pass"
else
BACKUP_RESULT="fail"
fi
VERIFY_DURATION=$(($(date +%s) - VERIFY_START))
echo " {\"test\": \"backup_integrity\", \"result\": \"${BACKUP_RESULT}\", \
\"duration_seconds\": ${VERIFY_DURATION}, \
\"backup_file\": \"$(basename ${BACKUP_FILE})\"}," >> "${TEST_LOG}"
# Test 2: Database restore to staging
echo "Testing database restore..."
RESTORE_START=$(date +%s)
# Restore latest backup to staging database
if pg_restore -h staging-db.internal -U restore_test \
-d dr_test_$(date +%Y%m%d) \
/backup/database/latest.dump 2>/dev/null; then
# Verify row counts match production (within 1% tolerance)
PROD_COUNT=$(psql -h db.internal -t -c \
"SELECT COUNT(*) FROM customers;" 2>/dev/null || echo "0")
STAGING_COUNT=$(psql -h staging-db.internal -t -c \
"SELECT COUNT(*) FROM customers;" \
-d dr_test_$(date +%Y%m%d) 2>/dev/null || echo "0")
if [ "${PROD_COUNT}" -gt 0 ] && \
[ $((STAGING_COUNT * 100 / PROD_COUNT)) -ge 99 ]; then
DB_RESULT="pass"
else
DB_RESULT="fail — row count mismatch"
fi
# Clean up staging test database
dropdb -h staging-db.internal -U restore_test \
dr_test_$(date +%Y%m%d) 2>/dev/null
else
DB_RESULT="fail — restore error"
fi
RESTORE_DURATION=$(($(date +%s) - RESTORE_START))
echo " {\"test\": \"database_restore\", \"result\": \"${DB_RESULT}\", \
\"duration_seconds\": ${RESTORE_DURATION}, \
\"rto_met\": $([ ${RESTORE_DURATION} -lt 3600 ] && echo true || echo false)}," \
>> "${TEST_LOG}"
# Test 3: Application service recovery
echo "Testing service recovery..."
SERVICE_START=$(date +%s)
# Restart application service and measure recovery time
systemctl restart app-service 2>/dev/null
sleep 5
# Check health endpoint
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
http://localhost:8080/health 2>/dev/null || echo "000")
SERVICE_DURATION=$(($(date +%s) - SERVICE_START))
if [ "${HTTP_CODE}" = "200" ]; then
SERVICE_RESULT="pass"
else
SERVICE_RESULT="fail — health check returned ${HTTP_CODE}"
fi
echo " {\"test\": \"service_recovery\", \"result\": \"${SERVICE_RESULT}\", \
\"duration_seconds\": ${SERVICE_DURATION}, \
\"health_check\": \"${HTTP_CODE}\"}" >> "${TEST_LOG}"
echo " ]," >> "${TEST_LOG}"
# Overall result
ALL_PASS=true
grep -q '"fail' "${TEST_LOG}" && ALL_PASS=false
echo " \"overall_result\": \"$(${ALL_PASS} && echo pass || echo fail)\"," \
>> "${TEST_LOG}"
echo " \"completed_at\": \"$(date -Iseconds)\"" >> "${TEST_LOG}"
echo "}" >> "${TEST_LOG}"
echo "DR test complete. Results: ${TEST_LOG}"
Monthly automated DR tests with documented results. The auditor does not need to take your word that you test your recovery procedures — they can see 12 months of test results with pass/fail outcomes, recovery durations, and data integrity verifications. If a test failed, the subsequent test shows the issue was resolved. This is the kind of evidence that turns a potentially contentious audit topic into a straightforward review.
Building your audit-ready infrastructure on Swiss servers provides specific advantages that simplify compliance narratives for both SOC 2 and ISO 27001:
Jurisdictional clarity for data residency controls. Both frameworks require you to document where data is processed and stored. Swiss dedicated servers give you an unambiguous answer: Switzerland, under the FADP, with EU adequacy recognition. No multi-region ambiguity, no cloud provider region selection complexity, no cross-border transfer concerns within the EU adequacy framework. Your Statement of Applicability (ISO 27001) and System Description (SOC 2) can reference a single, well-understood jurisdiction.
Physical security inheritance. Swiss data centres with ISO 27001 certification for their physical security controls provide inherited controls for your audit scope. Biometric access, 24/7 monitoring, visitor logging, environmental controls — these are controls you do not need to implement yourself because the data centre provides them with their own audit evidence. This reduces your control count and the scope of evidence you need to maintain.
Regulatory alignment across frameworks. Swiss hosting under the FADP aligns with GDPR requirements (adequacy decision), which aligns with SOC 2 Privacy criteria and ISO 27001 privacy controls. Instead of maintaining separate compliance narratives for each regulatory regime, your infrastructure supports a unified story: data is processed in Switzerland, protected by the FADP, recognised as adequate by the EU, and managed under controls that satisfy SOC 2 and ISO 27001 simultaneously.
No CLOUD Act exposure for access control narratives. When documenting your access control environment, the auditor will ask about third-party access — including government access. On US-based cloud infrastructure, you must document the CLOUD Act risk and explain your mitigations. On Swiss infrastructure, this entire risk category is addressed by jurisdiction: Swiss authorities require judicial process for data access, and foreign authorities must go through mutual legal assistance channels. That is a cleaner narrative than "we use encryption and hope it is sufficient to mitigate compelled disclosure."
If you are pursuing both certifications, here is the practical control mapping that lets you implement once and evidence twice:
• CC6.1 (Logical Access) ↔ A.9 (Access Control): SSH key-only authentication, named accounts, least privilege, access reviews. Same controls, same evidence, different report sections.
• CC6.7 (Data Confidentiality in Transit/Rest) ↔ A.10 (Cryptography): TLS 1.3, LUKS disk encryption, Vault Transit for application-level encryption. Your encryption evidence satisfies both.
• CC7.1 (Security Monitoring) ↔ A.12.4 (Logging): Centralized logging, audit trails, security event detection. The same Prometheus alerts and rsyslog configuration serve both frameworks.
• CC7.2 (Vulnerability Management) ↔ A.12.6 (Technical Vulnerability Management): Automated patch scanning, security update tracking, AIDE file integrity monitoring. One evidence stream, two controls.
• CC8.1 (Change Management) ↔ A.12.1.2 (Change Management): Git-based config management, PR reviews, deployment logs. The same CI/CD pipeline evidence maps to both.
• CC9.2 (Vendor Management) ↔ A.15 (Supplier Relationships): Vendor risk registry, DPA tracking, periodic reviews. One vendor management process, two compliance outputs.
• A1.1-A1.3 (Availability) ↔ A.17 (Business Continuity): Backup verification, DR testing, RTO/RPO validation. Same tests, same evidence, same results.
The overlap is roughly 70-80%. The marginal effort for the second certification — once the first is in place — is primarily documentation mapping and management system formality (ISO 27001's ISMS requirements). The technical controls are shared.
Based on real audit findings across fintech and SaaS companies, these are the issues that most frequently derail certifications:
Access reviews exist on paper but not in practice. You have a policy that says "access is reviewed quarterly." The auditor asks for the Q2 review. You produce a spreadsheet someone filled out last week from memory. The auditor notices the spreadsheet timestamp is two days ago, not during Q2. Finding issued. Fix: automated weekly access review scripts that generate evidence continuously, not quarterly manual efforts.
Change management has exceptions that swallow the rule. Your change management process requires PR review and approval. But "emergency changes" bypass the process — and 40% of your changes are classified as emergencies. The auditor sees that nearly half your production changes bypassed controls. Finding issued. Fix: emergency changes still go through the process, just with expedited timelines. The evidence must show authorization, even if it is retroactive within 24 hours.
Incident response plan has never been tested. You have a beautiful incident response document. The auditor asks when it was last tested. You say it was tested "informally" during the outage last March. They ask for the test plan, the test results, the lessons learned, and the plan updates based on the test. You have none of these. Finding issued. Fix: tabletop exercises every six months, with documented scenarios, participant lists, findings, and plan updates.
Vulnerability management has no defined SLA. You patch "when we get to it." The auditor asks for your vulnerability remediation SLA — critical within X days, high within Y days, medium within Z days. You do not have one. Finding issued. Fix: define and enforce remediation timelines — critical within 72 hours, high within 14 days, medium within 30 days. Track and report on compliance with these SLAs.
Backup testing proves backups exist but not that recovery works. You can show that backup jobs run nightly. The auditor asks when you last restored from backup to verify data integrity. You have never done this. Finding issued. Fix: monthly automated restore tests with data integrity verification, as shown in the DR test script above.
Building audit-ready infrastructure from scratch takes 4-6 months if you commit engineering resources and do not try to shortcut the process. Here is a realistic timeline:
• Month 1: Foundation. Deploy infrastructure on Swiss dedicated servers with LUKS encryption, hardened SSH, nftables firewall rules, and centralized logging. Implement the evidence collection framework from day one — every control you implement should generate evidence from the start. Write your risk assessment document.
• Month 2: Access control and change management. Implement Vault for secrets management and dynamic credentials. Move all configuration to Git with PR-based change management. Set up automated access review scripts. Define and document your access provisioning and de-provisioning procedures.
• Month 3: Monitoring and incident management. Deploy Prometheus with compliance-mapped alert rules. Write your incident response plan. Conduct your first tabletop exercise. Implement vulnerability scanning with defined remediation SLAs. Set up AIDE for file integrity monitoring.
• Month 4: Business continuity and vendor management. Implement automated backup verification and DR testing. Build your vendor risk registry. Execute DPAs with all in-scope vendors. Conduct your first DR test and document results.
• Month 5: Policy documentation and gap analysis. Write the policies that describe what you have built (not aspirational policies — descriptive ones). Conduct an internal gap analysis against SOC 2 TSC and ISO 27001 Annex A. Remediate any gaps found.
• Month 6: Readiness assessment. Engage your auditor/registrar for a readiness assessment (not the formal audit). They will identify any remaining gaps before the clock starts on your SOC 2 audit period or ISO 27001 Stage 1.
After month 6, your SOC 2 audit period begins — typically 6-12 months of evidence accumulation before the Type II assessment. For ISO 27001, the Stage 1 audit reviews your ISMS documentation, and Stage 2 (the main certification audit) evaluates implementation. Both can run concurrently with the same evidence base if your controls are designed for dual compliance from the start.
Let us be honest about costs, because compliance certifications are not cheap:
• SOC 2 Type II audit: $30,000-$80,000 for a mid-market SaaS company, depending on scope and auditor. Annual recurrence.
• ISO 27001 certification: $15,000-$40,000 for the initial certification audit (Stage 1 + Stage 2). Annual surveillance audits at roughly half that. Recertification every three years.
• Compliance tooling: $500-$2,000/month for evidence management platforms (Vanta, Drata, Secureframe). Optional — the infrastructure-native evidence collection approach described in this guide can replace much of this.
• Engineering time: 2-4 months of a senior engineer's time to build the infrastructure properly. This is the real cost, and it is the cost that pays back through reduced audit preparation time in subsequent years.
The infrastructure-native approach — building evidence collection into your hosting architecture rather than bolting on a compliance platform — has higher upfront engineering cost but lower ongoing cost. You are not paying a SaaS platform to screenshot your AWS console. You are generating structured, machine-readable evidence as a natural byproduct of running your infrastructure. The first audit is slightly more work to prepare. Every subsequent audit is significantly less.
SOC 2 and ISO 27001 are not security guarantees. A company with both certifications can still have terrible security practices — if the audit scope was narrow, the controls were minimal, or the auditor was lenient. Conversely, a company with excellent security might fail an audit on procedural grounds — missing a quarterly access review, not documenting an incident within the required timeframe, or lacking a formal risk assessment document.
The certifications prove that you have a structured approach to information security that a qualified third party has verified. For enterprise prospects, that is a meaningful signal — not because the certification guarantees security, but because it demonstrates that you take security seriously enough to submit to external scrutiny and invest in the operational discipline to maintain controls over time.
Building audit-ready infrastructure on Swiss managed servers gives you two advantages that compound over time. First, the technical controls are built into the infrastructure rather than documented on top of it — they do not drift because they are enforced by automation, not by policy awareness. Second, the jurisdictional simplicity of Swiss hosting under the FADP eliminates an entire category of compliance complexity around cross-border data transfers, foreign government access, and multi-jurisdiction regulatory navigation.
The result is infrastructure that passes audits not because you prepared well for the audit, but because the infrastructure was designed to be auditable from the first deployment. That is the only sustainable approach to compliance — and it happens to also be the approach that produces the best security outcomes.