Implement ISO 27001 information security management system controls. Covers Annex A controls mapping to engineering practices, risk assessment frameworks, evidence collection automation, and the patterns that turn security compliance from a binder on a shelf into working engineering practices.
ISO 27001 Engineering Controls are the technical and operational mechanisms that ensure an organisation’s information security management system (ISMS) meets the standard’s requirements. These controls are engineered into infrastructure, pipelines, and services to enforce, verify, and maintain compliance with ISO 27001 clauses 5.1 through 8.1. They matter when security is no longer a checklist but a continuous, auditable process—especially during audits, incident response, or when integrating new services into the ISMS.
Each ISO 27001 control must be tied to a responsible team, system, or engineer. This is not a spreadsheet exercise but a technical assignment embedded in the engineering lifecycle.
A.12.6.1 (Information Security Incident Management) is assigned via a control.yaml file in the root of the monorepo:control:
id: A.12.6.1
title: "Information Security Incident Management"
owner: "incident-response-team"
status: "active"
metadata:
description: |
Establish and maintain a process for identifying, reporting, assessing, and responding to information security incidents.
standard: ISO 27001
maturity: 3
evidence:
- "/docs/incident-handbook.md"
- "https://jira.example.com/browse/SEC-123"
This file is consumed by a compliance automation pipeline that generates a dashboard, triggers alerts, and exports evidence to audit repositories.
Evidence for a control must be collected, versioned, and verified automatically. The most common failure is evidence that is manually uploaded and never updated.
check-control.sh script runs daily and verifies that all required evidence is present and valid.#!/bin/bash
set -euo pipefail
CONTROL_ID="A.13.2.1"
EVIDENCE_DIR="/srv/compliance/evidence/$CONTROL_ID"
OUTPUT_JSON="/tmp/evidence-report.json"
if [[ ! -f "$EVIDENCE_DIR/control.yaml" ]]; then
echo "Error: control configuration missing in $EVIDENCE_DIR" >&2
exit 1
fi
# Validate event logs from the past 7 days
if ! journalctl --since="7 days ago" --unit=auth-service --output=short-precise | grep -q "SecurityEvent"; then
echo "Error: no SecurityEvent logs in auth-service for past week" >&2
exit 1
fi
# Check that all required files exist
declare -a required_files=(
"incidents-2024-01-01-to-2024-01-31.csv"
"incident-response-playbook.pdf"
"runbook-incident-handling.md"
)
for file in "${required_files[@]}"; do
if [[ ! -f "$EVIDENCE_DIR/$file" ]]; then
echo "Error: missing evidence file: $file" >&2
exit 1
fi
done
# Generate report
cat << EOF > "$OUTPUT_JSON"
{
"control_id": "$CONTROL_ID",
"timestamp": "$(date -Iseconds)",
"status": "pass",
"evidence": [
{
"file": "incidents-2024-01-01-to-2024-01-31.csv",
"size": "$(stat -c %s "$EVIDENCE_DIR/incidents-2024-01-01-to-2024-01-31.csv")",
"checksum": "$(sha256sum "$EVIDENCE_DIR/incidents-2024-01-01-to-2024-01-31.csv" | cut -d' ' -f1)"
}
],
"validation": {
"journalctl": "all SecurityEvent entries present",
"files": "all required files found"
}
}
EOF
# Upload to compliance storage
curl -X POST \
-H "Content-Type: application/json" \
-d @"/tmp/evidence-report.json" \
"https://compliance.example.com/api/evidence" \
-u "api-key:$(cat /etc/compliance/api-key)"
The script fails silently if any file is missing or if logs do not contain expected events, and the output is consumed by a compliance dashboard.
Many ISO 27001 controls are satisfied by configuration standards. The most common failure is a configuration drift between environments that goes undetected.
baseline.yml file defines the required configuration for A.14.1.1 (Security Configuration of Network Devices):control_id: A.14.1.1
title: "Security Configuration of Network Devices"
baseline:
firewall:
default_policy: "deny all"
logging: true
stateful: true
rules:
- protocol: tcp
port: 22
action: allow
source: "10.0.0.0/8"
description: "SSH access from internal network"
- protocol: tcp
port: 443
action: allow
source: "0.0.0.0/0"
description: "External HTTPS"
switch:
port_security: true
trunk_ports:
- 1
- 2
- 4
vlan:
- id: 10
name: "Management"
tagged: [1, 2, 4]
- id: 20
name: "Customer Data"
tagged: [3, 4]
This baseline is validated using a validate-configuration.py script:
import json
import yaml
import subprocess
import sys
def load_baseline(filepath):
with open(filepath, 'r') as f:
return yaml.safe_load(f)
def get_current_config(device_type):
# Query device via CLI, SNMP, or API
result = subprocess.run(
['snmpget', '-v2c', '-c', 'public', device_type, '1.3.6.1.4.1.8072.2.3.1.1'],
capture_output=True,
text=True,
check=False
)
if result.returncode != 0:
raise RuntimeError(f"Failed to query {device_type}: {result.stderr}")
return result.stdout
def compare_baseline(current, expected):
issues = []
for key, value in expected.items():
if key not in current:
issues.append(f"Missing key: {key}")
elif current[key] != value:
issues.append(f"Mismatch: {key} = {current[key]} (expected {value})")
return issues
def main():
baseline = load_baseline("baseline.yml")
current = get_current_config(baseline["control_id"])
issues = compare_baseline(current, baseline["baseline"])
report = {
"control_id": baseline["control_id"],
"timestamp": "2024-01-15T12:00:00Z",
"issues": issues,
"status": "pass" if not issues else "fail"
}
if issues:
report["status"] = "fail"
print(json.dumps(report, indent=2), file=sys.stderr)
sys.exit(1)
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
The script is run nightly via systemd timer and fails the CI pipeline if any configuration deviation is detected.
ISO 27001 requires that incident responses are documented, logged, and reviewed. A common failure is an incident that is resolved but not tracked against a control.
incidents.json file and trigger a control check.{
"id": "SEC-123",
"title": "Unauthorized Access to Customer Database",
"severity": "high",
"created": "2024-01-14T10:30:00Z",
"status": "in-progress",
"control_assignments": [
{
"control_id": "A.12.6.1",
"assigned_to": "incident-response-team",
"status": "in-progress",
"evidence": [
"https://docs.example.com/incident/SEC-123/initial-analysis.pdf"
]
}
],
"timeline": [
{
"event": "detection",
"timestamp": "2024-01-14T10:30:00Z",
"description": "SIEM alert from auth-service"
},
{
"event": "initial-response",
"timestamp": "2024-01-14T11:00:00Z",
"description": "Isolate affected hosts"
}
]
}
This file is updated by a create-incident.sh script that calls an internal API:
#!/bin/bash
set -euo pipefail
INCIDENT_ID="$1"
TITLE="$2"
SEVERITY="$3"
DATA="{\"id\": \"$INCIDENT_ID\", \"title\": \"$TITLE\", \"severity\": \"$SEVERITY\", \"control_assignments\": [{\"control_id\": \"A.12.6.1\", \"status\": \"in-progress\"}] }"
curl -X POST \
-H "Content-Type: application/json" \
-d "$DATA" \
"https://incident-api.example.com/v1/incidents" \
-u "api-key:$(cat /etc/incident/api-key)" \
-o /tmp/incident-response.json
# Validate response
if ! jq -e '.id == "'"$INCIDENT_ID"'"' /tmp/incident-response.json > /dev/null; then
echo "Error: failed to create incident $INCIDENT_ID" >&2
exit 1
fi
# Update control evidence
echo "Adding incident $INCIDENT_ID to control A.12.6.1" >> /var/log/compliance/updates.log
Failure modes include:
jq is not installed on the target system, causing JSON parsing to fail silently.curl fails to authenticate due to mismatched api-key value in file and environment.Control A.9.2.2 (User Access Management) requires that user access is provisioned, reviewed, and revoked consistently.
user-provisioning.yaml file defines the access model for a service:service: "customer-portal"
access_levels:
- name: "read-only"
permissions:
- "read:users"
- "read:orders"
roles:
- "viewer"
default: true
- name: "contributor"
permissions:
- "read:users"
- "read:orders"
- "write:orders"
- "create:orders"
roles:
- "editor"
- "contributor"
default: false
- name: "admin"
permissions:
- "all"
roles:
- "admin"
- "superuser"
default: false
This model is enforced via a provision-user.sh script:
#!/bin/bash
set -euo pipefail
USER_ID="$1"
ROLE="$2"
SERVICE="$3"
# Look up user in LDAP
LDAP_RESULT=$(ldapsearch -x -H ldap://ldap.example.com -b "ou=people,dc=example,dc=com" \
-D "cn=admin,dc=example,dc=com" -W -w "$LDAP_PASSWORD" \
"uid=$USER_ID" "cn" "mail" "department")
if [[ -z "$LDAP_RESULT" ]]; then
echo "Error: user $USER_ID not found in LDAP" >&2
exit 1
fi
# Extract attributes
USER_NAME=$(echo "$LDAP_RESULT" | awk '/^cn:/ {print $2; exit}')
USER_EMAIL=$(echo "$LDAP_RESULT" | awk '/^mail:/ {print $2; exit}')
DEPARTMENT=$(echo "$LDAP_RESULT" | awk '/^department:/ {print $2; exit}')
# Apply access level based on department
case "$DEPARTMENT" in
"Engineering")
ROLE="contributor"
;;
"Marketing")
ROLE="read-only"
;;
"Finance")
ROLE="admin"
;;
*)
ROLE="read-only"
;;
esac
# Send request to access control service
curl -X PUT \
-H "Content-Type: application/json" \
-d "{
\"user_id\": \"$USER_ID\",
\"service\": \"$SERVICE\",
\"role\": \"$ROLE\",
\"metadata\": {
\"provisioned_by\": \"script\",
\"source\": \"ldap\",
\"department\": \"$DEPARTMENT\"
}
}" \
"https://access.example.com/v1/users/$USER_ID" \
-u "api-key:$(cat /etc/access/api-key)" \
-w "\nStatus: %{http_code}\n"
# Verify success
if ! curl -s -f "https://access.example.com/v1/users/$USER_ID" | jq -e '.role == "'"$ROLE"'"' > /dev/null; then
echo "Error: failed to apply role $ROLE to user $USER_ID" >&2
exit 1
fi
The script silently fails if LDAP is unreachable, and the curl -f flag ensures that a non-2xx HTTP response fails the pipeline.
Each control must be assessed for maturity and health. A common failure is a control that is implemented but not reviewed or measured.
control-health.json file is generated by a check-health.sh script:{
"A.12.6.1": {
"status": "pass",
"last_updated": "2024-01-15T12:00:00Z",
"evidence_count": 8,
"last_incident": "SEC-123",
"review_date": "2024-01-31T00:00:00Z",
"maturity_level": 3,
"audits": [
{
"audit_id": "ISO-2024-01",
"date": "2024-01-10T09:00:00Z",
"findings": 2,
"status": "approved"
}
]
},
"A.14.1.1": {
"status": "warning",
"last_updated": "2024-01-14T10:00:00Z",
"evidence_count": 3,
"last_configuration_change": "2024-01-10T15:30:00Z",
"maturity_level": 2,
"issues": [
"Firewall rule missing for port 80",
"Switch VLAN 20 not tagged on port 4"
]
}
}
The script uses jq to extract and validate values:
#!/bin/bash
set -euo pipefail
OUTPUT_FILE="/tmp/control-health.json"
# Aggregate control data
jq -n \
--argjson controls "$(cat /var/lib/compliance/controls.json)" \
--argjson reports "$(cat /var/lib/compliance/evidence-reports.json)" \
'{
"A.12.6.1": {
"status": if (.controls[] | select(.id == "A.12.6.1") | .status == "pass") then "pass" else "fail" end,
"last_updated": (.[].timestamp | max),
"evidence_count": (.[].evidence | length),
"last_incident": (.[].control_assignments[].id | max),
"maturity_level": 3,
"audits": [
{
"audit_id": "ISO-2024-01",
"date": "2024-01-10T09:00:00Z",
"findings": 2,
"status": "approved"
}
]
}
}' \
> "$OUTPUT_FILE"
# Validate and upload
if ! jq -e '.["A.12.6.1"].status == "pass"' "$OUTPUT_FILE" > /dev/null; then
echo "Error: A.12.6.1 control is not passing" >&2
exit 1
fi
curl -X POST \
-H "Content-Type: application/json" \
-d @"/tmp/control-health.json" \
"https://compliance.example.com/api/health" \
-u "api-key:$(cat /etc/compliance/api-key)"
The script fails if jq is missing, if any control has a status of fail, or if the control-health.json file is malformed.
snmpget returns no output, but the script assumes it always returns data, leading to a missing baseline.api-key is stored in /etc/compliance/api-key, but the file contains api-key: abc123 instead of just abc123, causing 401 errors.maturity_level field is not updated in the JSON, leading to a control that is "mature" but not recorded as such.control_assignments field is not properly structured, causing the control dashboard to fail to render the correct assignment.These patterns form the backbone of engineering compliance with ISO 27001: not a static document, but a living, automated system that evolves with the organisation.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy