ISO 27001 Engineering Controls

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.

Implementing ISO 27001 Controls in Engineering Workflows

Define and Assign Control Ownership

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.

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.

Automate Control Evidence Collection

Evidence for a control must be collected, versioned, and verified automatically. The most common failure is evidence that is manually uploaded and never updated.

#!/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.

Enforce Configuration Baselines

Many ISO 27001 controls are satisfied by configuration standards. The most common failure is a configuration drift between environments that goes undetected.

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.

Integrate Incident Response with Control Tracking

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.

{
  "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:

Automate Access Control and Authorization

Control A.9.2.2 (User Access Management) requires that user access is provisioned, reviewed, and revoked consistently.

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.

Track Control Maturity and Health

Each control must be assessed for maturity and health. A common failure is a control that is implemented but not reviewed or measured.

{
  "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.

Common Failure Modes and Sharp Edges

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.